Photos
← gallery · Documentation

simple-photo-gallery

A fork-and-deploy photo gallery for Astro + GitHub Pages. Drop photos in a folder, push, get this site.

This site is its own documentation: everything you see — the masonry grid, the full-bleed viewer, the album pages, the lightbox — is what you get by forking the repository and pushing your own photos. No database, no CMS, no client framework: photos and optional markdown go in, a fully static site comes out.

Deploy your own

The quickest path is the scaffolder — it asks a few questions and sets up a fresh folder:

npm create simple-photo-gallery@latest my-photos

Or fork:

  1. Fork (or "Use this template") the repository.
  2. In your fork: Settings → Pages → Build and deployment → Source → "GitHub Actions". This is the only manual setting.
  3. Replace the contents of src/content/photos/ with your photos, edit gallery.config.ts, push to main.

The bundled workflow builds and publishes on every push, auto-detecting your Pages URL — user site, project site, or custom domain. To work locally:

npm install
npm run dev        # http://localhost:4321
npm run demo       # placeholder photos (with EXIF) if you have none yet
npm run album -- --title "Sicily" --dir ~/photos/sicily
                   # ingest real photos: resizes (EXIF preserved) + stubs index.md

Content model

Inside src/content/photos/, a folder is an album and a loose image is a single photo. Markdown is optional everywhere:

src/content/photos/
├── sicily-terrasini/        ← album → /sicily-terrasini/
│   ├── index.md             ← optional: title, date, writeup, captions, order
│   └── *.jpg
├── snow.jpg                 ← single photo
├── snow.md                  ← optional sidecar (same basename)
└── 2025-03-08-bicycle.jpg   ← date parsed from the filename

An album's index.md can curate everything:

---
title: "Sicily: Terrasini"
date: 2025-04-18
caption: First stop of a Sicilian Easter.   # album excerpt
cover: L1001243.jpg        # index thumbnail (default: first photo)
draft: false               # hide the whole album
tags: [travel, sicily]
photos:                    # explicit order — wins over everything
  - file: L1001243.jpg
    caption: Ceramic heads in a Terrasini storefront
  - file: L1001235.jpg
---
The markdown body is the album writeup, shown above the photos.

A loose photo's sidecar takes title, caption, alt, date, tags, draft.

Ordering

Per album (and for loose photos), the first applicable source wins:

  1. photos: list in index.md — curation beats everything.
  2. Numeric filename prefix — 01-…, 02-…
  3. EXIF DateTimeOriginal — chronological shooting order.
  4. Date in the filename — YYYY-MM-DD-…
  5. Filename, alphabetical — deterministic fallback.

The gallery index sorts items (singles + albums) newest-first; an album's date is its frontmatter date, else its newest photo.

EXIF

EXIF is read at build time — nothing ships to the client. It feeds ordering (above), naming (markdown title → XMP/IPTC title → humanized filename), tags (frontmatter ∪ IPTC/XMP keywords), and the caption templates below.

Configuration

One file — gallery.config.ts — controls the site:

KeyWhat it does
title · description · author Chrome title, <title>/meta, page header.
mode gallery (index + album pages) · single (the whole site is one album) · auto (single when content is exactly one album).
presentation Single mode only: grid (Grid ⇄ Viewer) or essay (writeup + scrolling photo feed).
chrome Shell variant: header (top bar — this site) · rail (vertical left rail) · frame (floating corner chips).
nav Manual chrome links (like "github" above). Markdown entry pages with nav: true are listed automatically before these.
mobileNav Menu on phones: kebab (default — a ⋮ button toggling a dropdown) or inline (links stay in the bar and scroll horizontally). Desktop always shows the inline links.
captionTemplate · exifTemplate Caption assembly. Tokens: {title} {caption} {date} {camera} {lens} {focal} {aperture} {shutter} {iso} {keywords}. Templates split on ·; a segment whose tokens all resolve empty is dropped, so missing EXIF degrades gracefully.
images Build-time renditions: thumb / viewer widths + sizes, full width (lightbox), quality. Sources can be full-resolution; sharp downscales (never upscales) to WebP.
dateFormat · locale Formatting for the {date} token.

Entry pages

A photo site usually needs a page or two that aren't photos — an about page, a colophon, an imprint. Drop a markdown file into src/content/pages/ and it becomes a standalone page at /<filename>/, rendered in this documentation style and linked from the site menu (the About page in the chrome above is exactly that — one markdown file, no template code):

---
title: About
description: Header excerpt + meta description.
mark: '✶'        # kick-line glyph (default ▤)
nav: true        # show in the chrome menu (default)
navLabel: about  # menu label (default: lowercased title)
order: 1         # menu position
draft: false
---

Standard markdown body — GFM tables, code blocks, images,
inline HTML all render like this page you are reading.

Menu order is entry pages first (by order), then the manual nav links from gallery.config.ts. A page whose filename matches an album folder collides — the build fails loudly rather than shadowing the album. For full control (custom components, tables like the ones on this page), write an .astro file in src/pages/ instead and link it via nav in the config — this docs page is built that way.

Using the gallery

  • Grid — row-first masonry. Albums carry a ▣ Album pill and link to their page; single photos open the Viewer in place.
  • Viewer — one photo per screen, normal scrolling. The current photo's slug lands in the URL, so /#<slug> deep-links work.
  • Album pages — writeup + photo feed; click a photo for the lightbox.
KeysViewerLightbox
/ j k previous / next photo previous / next photo
Home / End first / last photo first / last photo
Esc back to the grid close
Enter open the photo's action link

On touch, swipe left/right navigates the lightbox.

Customizing

  • Colors / typography — design tokens (light + dark) live in src/styles/tokens.css; components consume tokens only.
  • Fonts — Atkinson Hyperlegible Next ships with the repo; the mono face falls back to your system stack (add .woff2 files + @font-face rules to self-host one).
  • With a coding agentAGENTS.md in the repo documents the architecture, gotchas, design-system rules, and extension recipes (new chrome variants, caption tokens, tag pages…). And skills/photo-gallery/ ships a ready-made skill for agent assistants covering the whole lifecycle: set up a site from nothing, ingest albums (npm run album), curate captions and covers, customize the look — "publish these 10 photos to my gallery" as one delegated task.

The design system ("Modern TUI") has four rules: hard edges (border-radius: 0), solid shadows (hard offsets, no blur), type carries the design, and instant state (no transitions). Extracted from the gallery system of the author's Ghost theme and rebuilt on Astro content collections.

License

Code is MIT. The example photographs on this demo are © Arthur Soares and are not MIT — replace them with your own when forking.

░▒▓ EOF ▓▒░