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:
- Fork (or "Use this template") the repository.
- In your fork: Settings → Pages → Build and deployment → Source → "GitHub Actions". This is the only manual setting.
-
Replace the contents of
src/content/photos/with your photos, editgallery.config.ts, push tomain.
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:
photos:list inindex.md— curation beats everything.- Numeric filename prefix —
01-…,02-… - EXIF
DateTimeOriginal— chronological shooting order. - Date in the filename —
YYYY-MM-DD-… - 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:
| Key | What 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
▣ Albumpill 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.
| Keys | Viewer | Lightbox |
|---|---|---|
| ← → ↑ ↓ / 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
.woff2files +@font-facerules to self-host one). - With a coding agent —
AGENTS.mdin the repo documents the architecture, gotchas, design-system rules, and extension recipes (new chrome variants, caption tokens, tag pages…). Andskills/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.