# Configuration
A DD Photos site is driven by three config files. Both Docker and developer modes
use the same files — only the path conventions differ.
| File | Required | Purpose |
|--------------------|----------|-----------------------------------------|
| `albums.yaml` | yes | Albums, site settings, photo base paths |
| `site.env` | no | Deploy credentials |
**Docker mode:** `ddphotos init` creates a `config/` directory with a starter `albums.yaml`,
ready to edit directly — no copying needed (you'll create a `site.env` when you are ready
to deploy).
**Developer mode:** The repo's `config/` directory contains example files. Copy and edit
them to get started:
```bash
cp config/albums.example.yaml config/albums.yaml
cp config/site.example.env config/site.env
```
A good reference are the `sample/config/` files that drives the demo site at
[ddphotos.donohoe.info↗](https://ddphotos.donohoe.info).
---
## albums.yaml
The primary config file. See [config/albums.example.yaml](../config/albums.example.yaml)
for the full format and all options.
### Site Settings
The `settings:` block defines the site's identity and optional features:
```yaml
settings:
id: my-site # required; names the output directory
site_name: "My Photo Albums" # required
site_url: https://photos.example.com # required
site_description: "My photos" # required
copyright_owner: "Your Name" # required
copyright_year: 2020 # required
allow_crawling: false # set true to allow search engine indexing
site_title_html: "My Photos" # optional HTML for home page title
site_subtitle_html: "Since 2010" # optional HTML below the title
site_overview_html: "Welcome!" # optional HTML above album cards
```
| Setting | Required | Description |
|----------------------|----------|--------------------------------------------------------------------------------------------------------------|
| `id` | yes | Names the output directory; must be lowercase letters, digits, and hyphens |
| `site_name` | yes | Site title shown in the browser tab and OG tags |
| `site_url` | yes | Canonical base URL (e.g. `https://photos.example.com`); used in sitemap and OG tags |
| `site_description` | yes | Meta description and OG description for the home page |
| `copyright_owner` | yes | Name shown in the footer copyright line |
| `copyright_year` | yes | Start year shown in the footer copyright line |
| `descriptions` | no | Path to a [descriptions file](#album-descriptions), relative to the config dir; see that section for details |
| `allow_crawling` | no | Set to `true` to allow search engine crawling; adds `Sitemap:` to `robots.txt` (default: `false`) |
| `site_title_html` | no | HTML for the site title on the home page; falls back to `site_name` when omitted |
| `site_subtitle_html` | no | HTML rendered below the site title in a smaller font |
| `site_overview_html` | no | HTML rendered above the album cards (slightly larger than album descriptions) |
### Source Bases
The `bases:` block defines named paths to where your source photos live. Albums (and
the hero image) reference a base by name via the `base:` key, so you can keep album
entries short and change a root path in one place:
```yaml
bases:
drive: /Volumes/MyDrive/Photos # absolute path (e.g. an external drive)
cloud: /Users/me/Dropbox/Photos # absolute path (e.g. a cloud-synced folder)
local: photos # relative to the root DD Photos folder (sibling of config/)
albums:
- slug: patagonia
name: Patagonia
base: drive # references bases.drive
source: 2026-Patagonia # joined to the base path -> /Volumes/MyDrive/Photos/2026-Patagonia
```
Each base value is either an absolute path or a path relative to the root DD Photos
folder. An album's `source:` is joined to its named base; an album with no `base:` must
give an absolute path in `source:`.
In **Docker mode**, the `ddphotos` wrapper script reads these base paths and mounts them
into the container automatically, so the paths you list must exist on your machine.
### How Config Reaches the Frontend
`photogen` acts as a conduit between `albums.yaml` and the site frontend. The frontend
never reads `albums.yaml` directly — instead, `photogen` processes it and writes a set of
static JSON files that the browser fetches at runtime:
| File | Content | Encrypted when |
|-------------------------------------------------|------------------------------------------------------------------------|-------------------------------------------|
| `config.json` | Site ID, hero/CSS filenames, password hints, which albums file to load | Never — always plaintext (bootstrap file) |
| `html.json` / `html.enc.json` | `site_title_html`, `site_subtitle_html`, `site_overview_html` | Site password is set |
| `albums.json` / `albums.enc.json` | Album list with names, slugs, descriptions, date ranges, cover photos | Site password is set |
| `/index.json` / `/index.enc.json` | Per-album photo list: filenames, dimensions, dates, captions | Album or site password is set |
| `sitemap.xml` | URLs for each album, built from `site_url` | Never |
| `hero.jpg` | Cropped hero banner image | Never |
| `custom.css` | Copied from the file named in `settings.css` | Never |
`config.json` is the one file the frontend always fetches first, in plaintext, to
bootstrap the page. It tells the browser what site it is, whether albums are encrypted,
where to find the hero and CSS, and what hints to show before a password is entered.
The three `*_html` fields are the only settings that are encrypted when a site password
is set, since they may contain private links or contact details. All other settings
travel via `config.json` which is always plaintext.
### Hero Image
An optional full-width banner image can be displayed at the top of the home page.
Add a `hero:` block under `settings:`:
```yaml
settings:
hero:
image: my-banner.jpg # filename; joined to 'base' if set, else relative to config dir
base: drive # optional — same base map as album entries
crop: center # top | center | bottom (default: center)
```
`photogen` hard-crops the source image to 1600×250px and writes it as `hero.jpg` in the
albums output directory. The hero is never encrypted and takes priority as the `og:image`
on the home page.
To regenerate the hero without reprocessing albums or rebuilding indexes:
```bash
ddphotos photogen -- -hero-only # Docker mode
bin/photogen -hero-only -doit # developer mode
```
### Custom CSS
To override site styles, add a `css:` entry under `settings:`:
```yaml
settings:
css: custom.css # filename relative to this config dir
```
`photogen` copies the file to the site output as `custom.css`. The frontend injects it
site-wide as a `` after the built-in styles, so any rules inside it take effect as
normal cascade overrides. Redefining CSS custom properties (e.g. `--bg-color`,
`--text-color-2nd`) is the cleanest approach — no specificity battles needed.
### Password Protection
Encryption is enabled by adding a `passwords:` entry under `settings:` pointing to a
YAML passwords file (path relative to the config dir):
```yaml
settings:
passwords: passwords.yaml
```
The `-passwords` CLI flag overrides this at run time. When a passwords file is present,
`photogen` encrypts `albums.json` and each album's `index.json` using AES-256-GCM (keys
derived via PBKDF2-SHA256). Encrypted files are written as `.enc.json` alongside their
plaintext counterparts. The custom HTML fields (`site_title_html`, `site_subtitle_html`,
`site_overview_html`) are encrypted into `html.enc.json` and decrypted as part of the
same unlock step.
Decryption happens entirely in the browser using the Web Crypto API — passwords are never
sent to a server.
**Do not commit real passwords.** Store the passwords file outside the repo or in a
git-ignored directory (e.g. `.secrets/`).
#### Passwords File Format
```yaml
key: hmac-secret
site:
password: site-wide-password
hint: Optional hint shown in the password dialog
albums:
album-slug:
password: per-album-password
hint: Optional hint shown in the album password dialog
```
| Field | Description |
|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `key` | HMAC-SHA256 secret used to derive UUID-format WebP filenames for encrypted albums, preventing filename guessing (e.g. `IMG_3961.webp` becomes `3f8a1c2d-...webp`) |
| `site.password` | Encrypts `albums.json` and all per-album `index.json` files site-wide |
| `site.hint` | Optional hint shown in the site-wide password dialog (always visible, even before a password attempt) |
| `albums..password` | Per-album password; encrypts only that album's `index.json`. Falls back to `site.password` if not set |
| `albums..hint` | Optional hint shown in that album's password dialog |
Sample passwords files are in `sample/config/` — `passwords-all.yaml` (full site),
`passwords-uganda.yaml` (single album), and `passwords-keyonly.yaml` (a test fixture with a
`key` but no passwords, so nothing is encrypted). The first two contain demo-only passwords.
A passwords file that declares no `site.password` and no `albums..password` protects
nothing: the `key` only applies to albums that have a password, so every album stays public
and filenames stay unobfuscated. Such a site is reported as unencrypted in `config.json`,
which means no logout button is shown.
#### Frontend Behavior (Encrypted Sites)
When the frontend loads an encrypted page, it:
1. Reads `config.json` (always plaintext) to get the `siteId`, hints, and which albums
file to load (`albums.json` vs `albums.enc.json`). If `htmlFile` is set, also fetches
`html.json` (plaintext) or holds `html.enc.json` as a raw blob for later decryption.
2. Checks localStorage for a stored password scoped to the current `siteId`.
3. If a stored password decrypts successfully, both `albums.enc.json` and `html.enc.json`
are decrypted in parallel — the page renders with all content in a single DOM update,
with no flash.
4. If no stored password works, a full-screen `PasswordPrompt` overlay appears with a
lock icon, a password input, and an optional hint. A wrong password triggers a shake
animation; a correct one stores the password in localStorage and decrypts all content.
**Stored passwords and auto-unlock:** After a successful unlock, the password is saved
to localStorage so subsequent visits auto-decrypt without prompting. Append `?clear` to
any URL to clear all stored passwords and covers and return to the prompt:
```
http://localhost:5173/?clear
http://localhost:5173/albums/uganda?clear
```
**Cover flash prevention:** Album cover images are cached in localStorage after unlock,
as a visual indicator that an album is accessible.
An inline script in `app.html` runs synchronously before first paint, reading the cover
cache and setting CSS custom properties (`--ddp-cover-{slug}`) on ``. This means
the cover image is visible from the very first paint with no flash, even before Svelte
hydrates.
**localStorage key format** (useful for debugging):
| Key | Contains |
|-----------------------------|--------------------------------------------------------------------------------------------|
| `ddp_theme` | Light/dark mode preference (`light` or `dark`) |
| `ddp_site_id` | Current build token (`siteId` or `siteId:keyId`); triggers stale cache clearance on change |
| `ddp_site_{siteId}` | Site-wide password (encryption only) |
| `ddp_album_{siteId}_{slug}` | Per-album password for `slug` (encryption only) |
| `ddp_cover_{siteId}_{slug}` | Cached cover image URL for `slug` (encryption only) |
The `ddp_*` keys are scoped to `siteId` so that switching between builds (which use different
HMAC keys and produce different filenames) automatically invalidates stale cached data.
`?clear` removes all `ddp_*` keys (including `ddp_theme`), returning the site to its default state.
---
## Album Descriptions
Per-album descriptions are shown on the home page album cards and on each album page.
The preferred way to set them is inline in `albums.yaml`:
```yaml
albums:
- slug: patagonia
name: Patagonia
source: /photos/patagonia
description: Two weeks hiking the Torres del Paine circuit.
```
### descriptions.txt
As an alternative, descriptions can be kept in a separate file and referenced from
`albums.yaml` via `settings.descriptions`:
```yaml
settings:
descriptions: descriptions.txt
```
This is useful when you want to share one descriptions file across multiple config
files. See [config/descriptions.example.txt](../config/descriptions.example.txt) for
the format.
When both an inline `description:` and a `descriptions.txt` entry exist for the same
album, the inline value takes precedence.
---
## site.env
Holds deploy credentials — nothing that affects the built site itself.
See [Deploy Variables](ENV.md#deploy-variables-siteenv) for the full variable reference.
**rsync deployment:**
```bash
RSYNC_HOST=user@your-server.example.com
RSYNC_DEST=/path/to/your/web/root/
CLOUDFRONT_ID=YOUR_CLOUDFRONT_DISTRIBUTION_ID # optional; invalidates cache after deploy if you are using CloudFront
```
**S3 + CloudFront deployment:**
```bash
S3_BUCKET=your-s3-bucket
CLOUDFRONT_ID=YOUR_CLOUDFRONT_DISTRIBUTION_ID # optional; invalidates cache after deploy if you are using CloudFront
```
See [Deployment](DEPLOY.md) for full deploy details.