Film photography portfolio with visual essay editor
Based on revista-3 | Live at 120shots.com
A photography portfolio built with Astro, designed around film photography. Content lives as YAML files in the repo, photos are hosted on CloudFlare R2, and the site deploys as a static build to CloudFlare Pages.
The project includes a local visual CMS for composing and editing photo essays with drag-and-drop spread layouts.
yarn installCreate a .env file in the root directory:
R2_ACCOUNT_ID=your account ID
R2_ACCESS_KEY_ID=your access key
R2_SECRET_ACCESS_KEY=your secret access key
BUCKET_NAME=name of your bucket
BUCKET_PUBLIC_URL=public URL of your bucket
TOP_LEVEL_DIR=images/
GOOGLE_API_KEY=your google cloud api key # Optional: for Vision API image descriptionsyarn dev # Astro dev server
yarn cms # Photo essay CMS at localhost:4444
yarn build # Production build
yarn postbuild # Generate search index (Pagefind)
yarn preview # Preview production build
yarn lint # Format with PrettierThe Pagefind search only works after a full build (yarn build && yarn postbuild).
The recommended workflow for publishing new content:
Film rolls are created from a folder of photos using CLI scripts. The script processes images, uploads them to R2, and generates a YAML file with metadata.
yarn create-roll -p /path/to/photos -n ROLL-NAME -f film-stock -c "Camera Name"This uses Google Vision API by default to generate alt text descriptions (requires GOOGLE_API_KEY). Use --skipVision to skip this.
There are three ways to create essays:
From existing rolls (recommended for a quick start):
yarn create-roll-essay -r "ROLL-NAME1,ROLL-NAME2" -t "Essay Title"From a folder of photos (uploads + creates essay in one step):
yarn create-essay -p /path/to/photos -d upload-dir -t "Essay Title"From the CMS (select photos visually):
Open yarn cms, go to Essays, click "New Essay", select photos from any roll, and click Create.
yarn cmsThe CMS (localhost:4444) provides a visual editor for arranging essays:
- Drag photos from the sidebar into spread slots
- Choose from 8 spread layouts (single, duo, trio, and emphasized variants)
- Reorder spreads with drag handles
- Move or swap photos between slots
- Edit captions, metadata, cover photo, and tags
- Save writes directly to the YAML files with Ctrl/Cmd+S
See cms/README.md for full CMS documentation.
For a guided experience through any of the above workflows, use the /photo Claude Code slash command:
/photo # Interactive menu
/photo roll # Create a film roll
/photo essay # Create an essay from photos
/photo rollessay # Create an essay from existing rollsAll content is defined as Astro content collections with Zod schemas in src/content/config.ts.
YAML files defining visual stories with photo spreads. Each spread has a layout type and one or more photos.
Spread layouts:
| Layout | Photos | Description |
|---|---|---|
single |
1 | One photo, full width |
duo |
2 | Two photos side by side |
duo-h |
2 | Two photos stacked vertically (experimental) |
duo-l |
2 | Left photo emphasized (wider) |
duo-r |
2 | Right photo emphasized (wider) |
trio |
3 | Three photos in a row |
trio-l |
3 | Left photo large, two stacked right |
trio-r |
3 | Two stacked left, right photo large |
Essays support full-viewport scroll-snap navigation, optional captions per spread, keyboard controls, and mobile-responsive vertical stacking.
Example essay YAML
title: "Taipei Streets"
description: "Wandering through the urban layers of Taiwan's capital"
pubDate: 2024-01-20
author: paskal
rolls:
- 2021/TPE-01
filmStocks:
- gold-200
tags:
- street
- taiwan
spreads:
- layout: single
photos:
- src: https://cdn.120shots.com/images/TPE-01/photo-001.webp
alt: Taipei skyline
caption: "Looking up at Taipei 101"
- layout: duo
photos:
- src: https://cdn.120shots.com/images/TPE-01/photo-002.webp
alt: Street scene
- src: https://cdn.120shots.com/images/TPE-01/photo-003.webp
alt: Night market
caption: "Day and night"
- layout: trio
photos:
- src: https://cdn.120shots.com/images/TPE-01/photo-004.webp
alt: Temple
- src: https://cdn.120shots.com/images/TPE-01/photo-005.webp
alt: Incense
- src: https://cdn.120shots.com/images/TPE-01/photo-006.webp
alt: LanternsYAML files organized by year (e.g., rolls/2024/TPE-01.yaml). Each roll contains shot sequences with image URLs, dates from EXIF data, and optional labels/location metadata.
YAML files describing film stocks (brand, ISO, color type). Referenced by rolls.
MDX files with author profiles.
yarn create-roll -p /dir/with/photos -n ROLL-NAME -f film-stock -c "Camera" -rs| Flag | Description |
|---|---|
-p |
Photos source directory |
-n |
Roll name (also used as R2 subdirectory) |
-f |
Film stock ID (must match src/content/films/) |
-c |
Camera used |
-m |
Max image dimension in px (default: 2000) |
-r |
Rename files with roll name prefix + sequence number |
-s |
Add random suffix to filenames |
--skipVision |
Skip Google Vision API alt text generation |
Reads EXIF dates from photos. Requires R2 credentials in .env.
yarn create-essay -p /dir/with/photos -d upload-dir -t "Title" -m 2000| Flag | Description |
|---|---|
-p |
Photos source directory |
-d |
R2 upload subdirectory |
-t |
Essay title |
-m |
Max image dimension in px (default: 2000) |
-r |
Rename files with prefix + sequence number |
-s |
Add random suffix to filenames |
yarn create-roll-essay -r "ROLL1,ROLL2" -t "Title"| Flag | Description |
|---|---|
-r |
Comma-separated roll IDs |
-t |
Essay title |
yarn describe-images "https://cdn.120shots.com/images/roll/photo.webp"Uses Google Vision API. Options: --detailLevel (basic/detailed/comprehensive), --delay (ms between API calls).
yarn r2-stats --detailedOptions: --detailed, --prefix, --json, --limit.
yarn reformat -f path/to/file.yamlReformats roll or essay YAML with consistent formatting. Creates .bak backup by default (--noBackup to skip).
Build and deploy as a static site on CloudFlare Pages:
yarn build && yarn postbuildnpm create astro@latest -- --template paskals/120shots-websiteAfter creating your site:
- Delete example content from
src/content/photoessays/,src/content/rolls/, andsrc/content/authors/ - Create your own author profile under
src/content/authors/ - Set your site URL in
astro.config.mjs - Update metadata and favicon in
src/layouts/BaseLayout.astro - Create a CloudFlare R2 bucket and add credentials to
.env



