This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Read these first — they cover everything below in more depth and are kept more current than this file:
AGENTS.md— contributor and agent conventions, build/preview workflow, deployment, style rules, security notes.CODEMAP.md— where each piece of content lives underdocs/, howmkdocs.yml/mkdocs.deploy.yml/overrides/fit together, tooling overview.README.md— MkDocs install, local preview, deployment overview for new contributors.
If anything below conflicts with those files or with the Makefile, those win.
Public documentation for PhotoPrism, published at https://docs.photoprism.app/. It is a MkDocs Material-themed site — Markdown sources under docs/, rendered to static HTML. There is no application code; changes here are content, navigation, theme overrides, and build config.
Build tool: ProperDocs. The site is built with ProperDocs, a maintained drop-in fork of MkDocs 1.x (MkDocs core is EOL and its planned 2.0 drops the plugin system). ProperDocs reads our existing mkdocs.yml/mkdocs.deploy.yml unchanged, keeps every plugin and the llms.txt hook working, and silences the build-time MkDocs-2.0/ProperDocs warnings. The make targets and CI call properdocs (properdocs build / properdocs gh-deploy); mkdocs remains installed only as a library dependency of the Material theme.
The German translation lives in a separate repo, photoprism/photoprism-docs-de.
| Command | What it does |
|---|---|
make deps |
Debian/Ubuntu first-time setup: apt Python packages, then make upgrade |
make install |
Create venv/ and install MkDocs Material + requirements.txt (no apt) |
make upgrade |
Nuke venv/ and reinstall; use when dependencies drift or you want the latest Material |
make watch |
Alias for make serve — livereload on 0.0.0.0:8000, watches docs/, overrides/, mkdocs.yml |
make build |
Production render using mkdocs.deploy.yml → site/ (do not commit site/) |
make deploy |
properdocs gh-deploy --force --config-file mkdocs.deploy.yml — emergency manual publish only |
make merge |
develop → deploy merge that triggers the GitHub Actions publish pipeline |
make img-resize |
mogrify to cap screenshots at 1000x860; run after adding images to docs/user-guide/img, docs/user-guide/**/img, or docs/getting-started/nas/img/asustor |
make fix |
chown/chmod the tree when MkDocs can't read or write files |
make spellcheck |
Spell check docs/ with typos (config in _typos.toml); installs a pinned binary into bin/ first |
make vale |
Optional, under evaluation: prose-style lint of docs/ with vale (config in .vale.ini); never a gate |
make check-links |
Report internal links/assets in site/ that do not resolve (run after a build) |
make format |
Run all three Markdown formatters (format-whitespace, format-tables, format-artifacts). Each has a -check variant that reports drift without modifying files and exits non-zero when it finds any |
make muffet |
Crawl the built site with muffet (also checks in-page anchors); serves it locally and tears it down |
Link checking — make check-links. Resolves every site-relative src/href/poster/
data-src/srcset in the built site/ tree against the files on disk. No network, deterministic,
exits non-zero on a miss. make check-links-external adds an off-site probe that is advisory only.
It complements the build rather than duplicating it, and the three overlap only partly:
- The MkDocs build validates internal
.mdlinks and anchors —check-linksdoes not check anchors, so keep reading the build output. check-linkscatches what the build does not: missing images and other assets in the rendered output, plus external links. Its external verdicts read the response body rather than trusting the status code, because a 4xx carrying a full page is usually an SPA deep link that works for a real visitor (reportedSOFT), while a rendered error page is genuinely dead (reportedBROKEN).make muffetrunsmuffet, which crawls a served site and does check anchors. The target servessite/on127.0.0.1itself, crawls it, and tears the server down, so probes never touch production access logs. Override withMUFFET_PORT=/MUFFET_ARGS=.make install-muffetfetches a pinned, checksum-verified binary intobin/— no Go toolchain needed. Advisory: muffet judges by status code, so expect false positives on SPA deep links and bot-challenged hosts.- muffet cannot see JS-driven fragments. It looks for an element with a matching
id, so a fragment handled in JavaScript reports asid #x not found. Live example onphotoprism-web:/editions/#compareand/teams/#compareare opened byfrontend/src/site/nav.js, not by anid, and are not broken. Don't silence this with--ignore-fragments— that would also drop the anchor checking that found three genuine broken anchors here.
Sibling copies of scripts/check-links.js live in photoprism-web and photoprism-blog, kept
byte-identical apart from the header and the default build directory — fix one, copy to the others.
Reviewing make watch / make build output for build warnings (missing files, broken links in nav, unresolved anchors) is the main correctness check. make spellcheck is the one lint target intended to be relied on; make vale exists to evaluate prose-style linting and deliberately does not fail. The llms.txt build hook has its own unit tests — see below.
MkDocs Material Insiders is now public on PyPI, so no GH_TOKEN is required in .env.
Building — two options:
-
Ephemeral container (preferred — installs nothing on the host). Use the upstream
squidfunk/mkdocs-materialimage and add the repo's extra plugins fromrequirements.txtat run time. The image's entrypoint ismkdocs, so override it with--entrypoint shto runpipfirst:docker run --rm --entrypoint sh -v "$PWD":/docs -w /docs squidfunk/mkdocs-material:latest \ -c "pip install -r requirements.txt && properdocs build -f mkdocs.deploy.yml"
This is a complete, throwaway build env. It runs as root —
pip installneeds to write into the image, so-u "$(id -u):$(id -g)"is not an option here — and everything it creates is owned by root:site/,hooks/__pycache__, and.cache/. All are git-ignored. ⚠.cache/is the one that bites later: theprivacyplugin writes mirrored external assets there, so a subsequent host build fails withPermissionError— but only once a new external asset appears, which is why a root-owned.cachecan sit unnoticed for months while every build succeeds. Runsudo chown -R "$(id -un):$(id -gn)" .cache site hooks/__pycache__afterwards, ormake fixto chown the whole repo. Note: the repo's ownDockerfileisFROM squidfunk/mkdocs-materialand nothing else — not maintained; don'tdocker buildit (it lacksmkdocs-redirects/mkdocs-tooltips). The upstream image is current and maintained — a different thing. -
Host
venv.make deps(first time), thenmake watch(livereload) ormake build. Fine on a dev machine; the container just avoids installing a Python toolchain on the host.
The llms.txt build hook (see LLMS.md) has unit tests that need no MkDocs install: cd hooks && python3 -m unittest test_llmstxt.
Two MkDocs configs, both must stay in sync.
mkdocs.yml— base config:nav:, theme options, plugins (search,redirectswith the dev redirect map), Markdown extensions, metadata, edit links. Thenav:map is the sole source of truth for site navigation — a new page is invisible until registered there.mkdocs.deploy.yml— inherits frommkdocs.yml, re-declares the plugins with the production redirect map, and adds theprivacyplugin (mirrors external assets at build time) and ahooks:entry (hooks/llmstxt.py, which generates/llms.txt+/llms-full.txt— seeLLMS.md). Used bymake buildandmake deployand by the CI pipeline. Production-only concerns live here, not in the base config, somake watchstays fast.
When you add or rename a redirect, update the redirect entries in both configs so local previews and production match. Same for any nav changes that affect URLs.
Directory ≠ nav hierarchy. Folder names mirror the canonical URL path, which is sometimes shorter than the nav label. Example: docs/release-notes.md renders at /release-notes/ even though its nav entry is under "User Guide > Release Notes". Preserve these short-URL exceptions when restructuring.
Content lives under docs/ only. Top-level content folders match the main nav: getting-started/, user-guide/, developer-guide/, plus landing/legal pages (index.md, known-issues.md, release-notes.md, credits.md, license/). Section-specific subfolders (organize/, search/, advanced/, proxies/, ai/, vision/, api/, media/, metadata/, …) have their own local img/ directories — store images next to the Markdown that references them, not in the global docs/img/.
Drafts go in todo/, not docs/. todo/ is the staging area for work-in-progress pages (e.g. todo/developer-guide/setup-fedora.md). They are not served. Promote into docs/ and register in mkdocs.yml when ready.
Theme and template overrides.
overrides/main.html— OG/Twitter cards, favicons, analytics (a.photoprism.app), announcement banner, and the site-wide<meta name="keywords">(set in theextraheadblock with apage.meta.keywordsfront-matter override, mirroring howdescriptionis derived fromsite_description). Review light and dark themes and the approved-domains list when editing.overrides/partials/copyright.html— footer text.docs/css/custom.css— scoped overrides for Material; keep selectors tight.
site/ and venv/ are build artifacts. Never commit them and never edit site/ by hand.
Work happens on develop. Merging develop → deploy (e.g. make merge) triggers the GitHub Actions pipeline (.github/workflows/ci.yml) which installs mkdocs-material + requirements.txt and runs properdocs gh-deploy --force --config-file mkdocs.deploy.yml, publishing to gh-pages. The web2 server then pulls gh-pages every 5 minutes and serves docs.photoprism.app. So: deploy branch updates are production releases, not a staging environment.
Standard release flow — always perform all steps so develop and deploy contain the same commits and the local checkout ends on develop:
- Commit on
developandgit push origin develop. - Run
make merge(or, manually:git checkout deploy && git pull origin deploy && git merge develop && git push origin deploy). This fast-forwardsdeployto matchdevelopand kicks off the CI build. - Return to
developlocally:make mergedoes this automatically with a trailinggit checkout develop; if you ran the steps manually, switch back yourself so the next edit session doesn't accidentally land ondeploy.
Verify with git rev-parse develop deploy origin/develop origin/deploy — all four should print the same SHA when the release is clean.
make deploy pushes to gh-pages from your laptop — reserved for emergencies, and coordinate with maintainers first so the CI pipeline does not overwrite your push.
deploy is a protected branch. Pushes print remote: Bypassed rule violations for refs/heads/deploy: and Cannot update this protected ref. in the log but still succeed with exit 0 — the user has an admin override. Don't treat those lines as errors.
- The user guide describes the current stable release; the developer guide tracks
develop(Michael, 2026-09-23). So a flag, command or option that exists only in a preview build may be documented underdocs/developer-guide/straight away, and must not appear underdocs/user-guide/until it ships in a stable release. ⚠ The failure is silent and user-facing: a reader on stable follows the page and the flag does not exist. It happened on 2026-09-21 —faces reset [--yes]was added to the user guide's CLI reference while--yeswas preview-only, published live, and reverted on 09-23 in338d17f4. Check before documenting a CLI flag: run it against the stable image (docker run --rm --pull=always --entrypoint photoprism photoprism/photoprism:latest <cmd> --help), not against a checkout or a preview. TheDevelopment Previewblock inrelease-notes.mdis the exception — it is supposed to describe unreleased work. docs/getting-started/config-options.mdis auto-generated on release. Do not hand-edit it — changes will be overwritten. When a new env var or flag needs to surface in docs, update the manually-maintained page that describes the feature (e.g.docs/developer-guide/vision/face-recognition.md,docs/user-guide/settings/advanced.md) and link to the auto-generated table for the full list.- Package READMEs in the main PhotoPrism repo are canonical. Before rewriting a developer-guide page, check your local
photoprism/photoprismcheckout atinternal/<package>/README.md— e.g.internal/ai/face/README.md,internal/thumb/README.md,internal/meta/README.md. These are kept more current than docs and usually contain the exact thresholds, benchmarks, and test recipes you need. Link to them from the dev-guide page rather than duplicating the content. - Don't rewrite historical
release-notes.mdentries to reflect later removals or changes. Past entries (e.g. "Replaceddisintegration/imaging…") stay as-is — they are the record of what shipped that release. Update the current pages that describe the feature instead. - The user sometimes edits docs manually between sessions. Before starting work,
git status+git pull --ff-only origin developso local changes don't trample their edits (and so you aren't rebuilding content that has already been revised).
- Chicago-style Title Case on every heading, nav label, and link title. Rules are spelled out in
AGENTS.md. Always spell the product namePhotoPrism(proper noun, exception to generic rules). - Refresh
**Last Updated:**at the top of a doc whenever you change its contents (format:January 20, 2026, no time). Leave it alone for whitespace-only or pure-formatting edits. - Prefer Markdown over raw HTML. Use Material components (admonitions, tabs, tooltips, mermaid) already configured in
mkdocs.ymlrather than inventing shortcodes. - Filenames are lowercase-kebab (
snake-case.md); directories mirror nav labels. - RFC 3339 UTC timestamps in request/response examples; valid-looking IDs/UIDs/UUIDs in code samples.
- CLI examples: flags before positional arguments unless the command requires otherwise.
Concise, imperative subjects with a one-word Prefix: indicating scope. Subject ≤80 chars. Examples from recent git log:
Docs: Update release notes for preview 260420-7d39b2d9f
docs: point developer guide at Gemma 4 and frob/qwen3.5-instruct:4b
Do not append Co-Authored-By: Claude … trailers. No emojis in commit messages. Reference issue or PR IDs when relevant (e.g. Docker: Use two stage build to reduce image size #123 #5632).
Only create, edit, close, reopen, or relabel GitHub issues when explicitly asked. When asked:
- Title: imperative mood,
Prefix: Subject(e.g.Search: Add filter for RAW image formats). - Body starts with a fully-bold one-sentence User Story:
**As a <role>, I want <goal>, so that <outcome>.** - Body ends with an Acceptance Criteria
- [ ]checklist where each item uses one ofMUST/SHOULD/MAY.