- 28 more of the operator's own public images (42 -> 67), alts from his captions - remove group-feier, group-outdoor, group-posiert (wedding guests visible) - studio section now offers Studio Neumuenster and Mobiles Studio - deploy: --force-recreate on release and rollback, plus in-container content probe - verify: DESFOTO_BASE dry-run, redirect, asset and privacy checks - tests: 57 OK (3 new content guards, 2 deploy/verify regression guards)
6.4 KiB
desfoto.de — Agent Guidelines
What this repository is
The complete source of https://desfoto.de, the photography and video brand of
Denny Schulz (Neumünster). It is a hand-written static site: no framework, no CMS,
no runtime dependencies. src/ holds Python builders and the editorial content,
scripts/build-site.py renders everything into the committed site/ directory,
and nginx serves site/ from a container behind the shared VPS Traefik instance.
dennyschulz.de is a separate site: nothing here may modify it, proxy it or
hotlink its assets. Linking out to it from the footer is an explicit operator
request and is the only permitted reference (see "Editing rules").
Structure
/— hero, two pillars (Shootings = photo, Videoprojekte = video), portfolio gallery, music video, mobiles Studio, process, facts./shootings/— the umbrella for all photographic work: Businessfotografie, Portrait & Model, Familien & Paare, Minishootings, plus the mobiles Studio./fotografieand/fotografie/are 301-redirected here bynginx.conf./video/,/social-media/,/projekte/(music + free work + pro bono),/ueber/.
Commands
python3 scripts/fetch-assets.py— download pool images, Google Fonts subsets and licences intoassets-src/(idempotent;assets-src/is not committed).python3 scripts/build-site.py— rebuild all ofsite/. Printsbuild okand a link/alt check; exits non-zero on any broken internal reference..ocauto/qa— authoritative deterministic QA: builds twice to prove the output is reproducible, runsnginx -tanddocker compose config, thenpython3 -m unittest discover -s tests.- Local runtime:
docker compose -f compose.yml up -d→ http://127.0.0.1:18430 - UI smoke:
/home/king/bin/oc-ui-smoke http://127.0.0.1:18430/ - Release:
/home/king/bin/oc-release /home/king/projects/desfoto "<msg>" <files...>
Editing rules
- Content lives in
src/content.pyandsrc/pages.py; never hand-editsite/.site/is generated output that happens to be committed for deployment. - Never invent facts. Prices, testimonials, client names, awards and biography dates
are intentionally absent; do not add them without verified information. Legal data
in
src/content.py:LEGALmirrors the operator's own Impressum (§ 5 DDG). The portfolio must reflect the operator's real work (musicians, live shows, free and artistic series, business shoots) — never invent trips, locations or jobs (an earlier build captioned an image as a "Japanreise"; that was invented and must not return). - Keep the design language: paper/ink/rust/teal tokens from
src/theme.py, rounded cards, hairline dividers, Fraunces for display and Manrope for text. - Images come from the operator's own pool through
src/images.json. Only add an entry if the operator holds the rights; always give a meaningful Germanalt."source": "legacy"marks images fetched from the operator's own site (dennyschulz.de/legacy/<file>) — they are written intoassets-src/images/and never hotlinked at runtime.alttexts must come from the operator's own captions. - The operator's original logo lives in
assets-src/brand/logo.png; the build deriveslogo-160.png, the favicons, the apple-touch icon andfavicon.svgfrom it. Never redraw or guess a replacement mark. - The footer's "Netzwerk" column links to
https://www.dennyschulz.de/andhttps://dennyapp.de/because the operator asked for it; these are the only external links allowed and they must keeptarget="_blank" rel="noopener". Nothing else may referencedennyschulz.de(no assets, no fetches, no rewriting). - Animations are progressive enhancement: they live in CSS plus the guarded
site.jsblock, every page carries the<noscript>reveal fallback, and theprefers-reduced-motionblock must stay the last rule insrc/theme.pyso it wins over the motion definitions. - Never hide the element you observe: Chromium folds a target's own
clip-pathinto theIntersectionObservergeometry, so a closed mask never reports as visible and the content stays hidden forever..reveal--masktherefore clips its children. - Never nest
<a>inside<a>(or any other invalid nesting). The parser closes the outer anchor and the surrounding grid falls apart — that is what happened to the two homepage pillars. Cards with their own links use the stretched-link pattern (.pillar__go::after);tests/test_site.pyguards this for every page. - Two-space indentation,
from __future__ import annotations, type hints on public functions, no new third-party Python packages (Pillow is the only dependency).
Privacy contract
The /datenschutz/ page makes concrete promises. Any change that would break one of
them is a defect, not a style question:
- no cookies, no
localStorage, no analytics, no tracking; access_log offinnginx.conf— never enable request logging;error_log /dev/null crit;innginx.conf— never write an error-log file either;- fonts, scripts, styles and images are served from this origin only;
- YouTube is embedded exclusively via the click-to-load facade
(
youtube-nocookie.com) — no iframe before the click, no preconnect; - the contact form never posts anywhere; it composes a local
mailto:draft (the non-JS fallback is amailto:form action, which also stays on the device).
tests/test_site.py enforces these promises; extend it when you add a new flow.
Deployment
.ocauto/deploy <sha> ships the committed build to /home/denny/stacks/desfoto on
prod-main, snapshots the previous release into /home/denny/stacks/desfoto-releases/
and removes the obsolete desfoto.de → dennyschulz.de labels from /srv/stack.
.ocauto/verify <sha> proves the live site matches the release. Details, including
rollback, are in docs/deployment.md. Traefik is shared infrastructure: route through
labels, never restart or recreate the proxy.
The deploy replaces site/ (new inode) and rewrites nginx.conf, so it runs
docker compose up -d --force-recreate: a container that is merely "running" keeps
the deleted directory bind-mounted and then serves an empty document root — every
path 404, while Compose reports no change and the stale health status still says
healthy. The recreate is followed by an explicit in-container request for /, and
tests/test_site.py guards the flag. Never remove either check.