Building a personal photo stream for Astro/Cactus-theme
/ 3 min read
Table of Contents
After looking around for a bit, I wasn’t happy with the photo extensions I found for AstroJS. So I finally built my own photo stream. The result is a setup that automatically extracts metadata, generates AI descriptions, creates static maps, and organizes photos with proper pagination and filtering. The overall workflow looks like this:
- I use Apple Photos to manage all my photos as a single source of truth. The caption field is used for minimal description hints to be passed on for the system prompt to auto-generate descriptions later on. I also use Apple Photos to set locations for photos taken with DSLRs without native GPS.
- Then photos get exported to a source folder in the AstroJS project. I use a folder structure like
./2025/07/25/DSC_1234.jpg
but you can use whatever you like. - Next step is running the script
pnpm photos:generate
. This extracts 1) EXIF data, 2) runs geo locations against the OpenCage Geocoding API to get human readable locations, uses 3) Geoapify API to create a static map image with marker for the location and 4) uses the Anthropic Claude API to generate an image description. The output of this scripts are markdown files with frontmatter for the metadata. - AstroJS then picks up these markdown files as a collection and will create a photo stream page, pages for all tags used, and pages for each individual photo.
This means I run pnpm photos:generate
locally on demand to generate the markdown files to be in control when I want to update and hit all those APIs. The AstroJS build process picks up the updates to generate the HTML files to be deployed.

Metadata generation
The system handles everything from EXIF extraction to AI-powered content analysis:
Smart Metadata Generation: A TypeScript script processes photos to extract technical data (camera, lens, settings, GPS coordinates) and combines it with Claude API analysis for descriptions, titles, and contextual tags.
Privacy-Focused Geolocation: The OpenCage Geocoding API is used to reverse-geocode GPS coordinates to human-readable locations while excluding street addresses for privacy. Geoapify API is used for static map generation.
Responsive Gallery: Built with Astro components, the photo stream uses a responsive grid layout with hover effects and proper image optimization. Individual photo pages show full technical details alongside AI-generated descriptions.
UI Implementation
Content Collections: Astro’s content collections handle the photo schema with support for both local images and external URLs. The schema includes camera equipment, technical settings, GPS coordinates, and AI-generated metadata.
Component Architecture: The system uses three main Astro components - PhotoCard
for grid display, PhotoStream
for pagination, and MapImage
for location display. Each handles both local ImageMetadata objects and external URL strings.
Location Privacy: The OpenCage geocoding API provides configurable precision levels, and the script the creates the location string tries to prioritize publicly known landmarks over private addresses.

Performance Considerations
With the need to support potentially thousands of photos, the workflow uses several optimizations:
- Lazy loading for images with proper width/height attributes
- Pagination with 12 photos per page to keep load times reasonable
- Image compression for API submissions (progressive quality reduction to stay under 5MB limits of Claude’s API)
- Static generation of all pages at build time rather than runtime processing
What’s Next
I’ll play around with this a bit more and will tweak a few things, then I plan to look into how I could release this as a AstroJS integration or maybe an extension to Cactus theme. In the meantime, have a look at my new photo stream: