See WERF.md for werf reference (binary, config, templates, variables, filters, quirks).
portfolio/
site/
_config.yml # title: "photos by matt"
_layouts/
default.liquid # HTML shell — header/nav/footer wrapper
photo.liquid # standalone full-bleed layout (no default wrapper)
gallery.liquid # wraps content in .gallery grid
_includes/
head.liquid # meta, Google Fonts (Caveat), CSS link
nav.liquid # site title + nav links
photo_card.liquid # card with hover overlay (no visible caption)
_posts/ # one .md per photo, YYYY-MM-DD-slug.md
_pages/
index.html # home gallery (all photos, uses gallery layout)
about.md
[tags].html # auto-generates one page per tag
css/
main.css # single file, no framework
public/
photos/
originals/
YYYY-MM-DD/ # scans grouped by date
thumbs/
YYYY-MM-DD/ # generated by generate-thumbs.sh, mirrors originals/
_headers # Cloudflare CDN cache policy
uploads/
YYYY-MM-DD/ # drop zone: originals + .jpg.xmp sidecars (gitignored)
processed/
YYYY-MM-DD/
mains/ # 80% quality re-saves (temp, deleted after upload)
thumbs/ # 50% quality re-saves (temp, deleted after upload)
scripts/
build.sh # Cloudflare entry point (download werf, thumbs, build)
start.sh # local dev (download werf if stale, watch mode)
new-photos.sh # consolidated upload workflow (process → R2 → post stubs)
generate-thumbs.sh # legacy: thumbnail generation from site/public/photos/
new-photo-posts.sh # legacy: post stubs from site/public/photos/originals/
extract-exif.sh # optional EXIF reader (film scans rarely have useful data)
WERF.md
README.md
- Put JPEGs + matching
.jpg.xmpsidecars inuploads/YYYY-MM-DD/ - Run a safe local pass:
./scripts/new-photos.sh YYYY-MM-DD --dry-run - If output looks good, run publish:
./scripts/new-photos.sh YYYY-MM-DD - Open new files in
site/_posts/and fill remaining fields (description,tags,exposure_compensation)
scripts/new-photos.sh — primary upload workflow (see New photo workflow):
- Reads originals from
uploads/YYYY-MM-DD/(never touchessite/) - Produces mains (80% quality, max 2000px wide) and thumbs (50% quality, max 800px wide) in
processed/YYYY-MM-DD/ - Parses darktable XMP sidecars (
<file>.jpg.xmp) to pre-populate frontmatter - Uploads to R2 via
wrangler r2 object put; tracks per-file failures without aborting - Creates
site/_posts/YYYY-MM-DD-<slug>.mdstubs (skips existing) - Deletes
processed/YYYY-MM-DD/on full success;uploads/YYYY-MM-DD/is kept - Idempotent: skips already-processed files, skips existing post stubs
--dry-runflag to run in local mode (publishes tosite/public/photosand uses/public/photos/...URLs)--rewrite-urlsflag to updateimage/thumbURLs in existing posts for that date- Prerequisites: ImageMagick (
magickorconvert),wrangler
scripts/generate-thumbs.sh (legacy, superseded) — generates thumbnails from site/public/photos/originals/
scripts/new-photo-posts.sh (legacy, superseded) — creates post stubs from site/public/photos/originals/
scripts/extract-exif.sh — optional; prints raw EXIF to stdout for a single image. Film scans rarely have useful EXIF.
---
layout: photo
title: "2026-02-24 / 0001_37"
date: 2026-02-24
film_name: Kodak Gold 200
film_format: 35mm
film_speed: 200
film_type: colour negative
developed_by: AG Photolab
exposure_compensation: box # box / +1 / -1 / +2 etc.
camera: Olympus OM-1
lens: Zuiko 50mm f/1.4
location: London
tags: # space-separated, drives auto-generated tag pages
image: https://pub-d1e192acd3c5456eb06f306d0bd48e3d.r2.dev/photos/originals/2026-02-24/0001_37.jpg
thumb: https://pub-d1e192acd3c5456eb06f306d0bd48e3d.r2.dev/photos/thumbs/2026-02-24/0001_37.jpg
description:
---XMP note: new-photos.sh auto-populates film_name, film_format, film_speed, film_type, developed_by, camera, lens, and location from darktable XMP sidecar hierarchical subjects. Fields not present in the XMP are left blank for manual entry.
Fonts
Caveat(Google Fonts, handwritten) — site title onlyGeorgiaserif — photo card titles on hover overlay- System sans-serif — body, nav, meta
Gallery
- CSS Grid, 6 equal columns, fixed row height (280px)
- Varying column spans per card:
span 2, 1, 1, 1, 2, 1repeating — gives varied widths at uniform height - Last card always
grid-column-end: -1to fill remaining row space object-fit: coveron all images- Hover: image scales up, gradient overlay fades in with title + meta in white
Photo page
- Standalone layout (
photo.liquid, not wrapped indefault) - Image fills full viewport (
100dvh), black background,object-fit: contain - Floating nav bar at top fades into image (gradient overlay)
- Scroll down for title, metadata table, tags — dark/moody style
Color scheme
- Light:
#fafafabg,#111fg - Dark (
prefers-color-scheme):#111bg,#e8e8e8fg - Photo page always dark (
#000bg)
| Setting | Value |
|---|---|
| Pages project name | photosbymatt |
| Production URL | https://photosbymatt.pages.dev |
| Build command | bash scripts/build.sh |
| Build output directory | dist |
| Root directory | / |
scripts/build.sh downloads the werf Linux binary (tagged by date, requires GITHUB_PAT), generates thumbs, builds the site, then copies site/_headers to dist/_headers.
/public/photos/*
Cache-Control: public, max-age=31536000, immutable
/css/*
Cache-Control: public, max-age=86400
Cloudflare Pages only applies custom headers from an output-root file named _headers. This repo keeps the source headers in site/_headers; the build script copies it to dist/_headers so image/CSS cache policies are actually applied.
max-age=31536000, immutable means images can be cached by browsers/CDN for up to one year, so replace image URLs (or filenames) when publishing updated versions of an image.
- Fill in metadata (title, film, camera, lens, location, tags) for the 19 real posts
- Wire up Cloudflare Pages (manual, in dashboard)
- Add favicon
- About page content