Files
desfoto/AGENTS.md
2026-09-25 20:08:25 +02:00

8.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, the four Arbeitsbereiche (Fotografie, Video, Musik & Bühne, Projekte), the bookable services ("Was du bei mir buchen kannst"), the portfolio gallery ("Was ich außerdem mache"), music video, mobiles Studio, process, facts.
  • /fotografie/ — the umbrella for all photographic work: Businessfotografie, Portrait & Model, Familie, Minishootings, plus the mobiles Studio. /shootings, /shootings/ and /shootings.html are 301-redirected here by nginx.conf, and the old /fotografie.html is canonicalised to /fotografie/.
  • /familie/ — family photography (no couples: Paare and Engagement stay on dennyschulz.de). /familien-und-paare, /familien-und-paare/ and /familien-und-paare.html are 301-redirected here.
  • /musik-und-buehne/ — concerts, band portraits and the Thjódrörir music video.
  • /video/, /social-media/, /projekte/ (music + free work + pro bono), /ueber/.

Commands

  • python3 scripts/fetch-assets.py — download pool images, Google Fonts subsets and licences into assets-src/ (idempotent; assets-src/ is not committed).
  • python3 scripts/build-site.py — rebuild all of site/. Prints build ok and 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, runs nginx -t and docker compose config, then python3 -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.py and src/pages.py; never hand-edit site/. 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:LEGAL mirrors 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 German alt. "source": "legacy" marks images fetched from the operator's own site (dennyschulz.de/legacy/<file>) — they are written into assets-src/images/ and never hotlinked at runtime. alt texts must come from the operator's own captions.
  • The operator's original logo lives in assets-src/brand/logo.png; the build derives logo-160.png, the favicons, the apple-touch icon and favicon.svg from it. Never redraw or guess a replacement mark.
  • The footer's "Netzwerk" column links to https://www.dennyschulz.de/ and https://dennyapp.de/ because the operator asked for it; these are the only external links allowed and they must keep target="_blank" rel="noopener". Nothing else may reference dennyschulz.de (no assets, no fetches, no rewriting).
  • Animations are progressive enhancement: they live in CSS plus the guarded site.js block, every page carries the <noscript> reveal fallback, and the prefers-reduced-motion block must stay the last rule in src/theme.py so it wins over the motion definitions.
  • Never hide the element you observe: Chromium folds a target's own clip-path into the IntersectionObserver geometry, so a closed mask never reports as visible and the content stays hidden forever. .reveal--mask therefore 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 the redesign replaced. Cards with their own links use the stretched-link pattern (.pillar__go::after, .link-arrow) or keep the link in the card foot; tests/test_site.py guards this for every page.
  • Brand separation: desfoto is the brand, Denny Schulz Fotografie is only the legal entity. The legal name may appear in the Impressum, the Datenschutz page and the JSON-LD legalName — nowhere else. The footer carries exactly one ownership sentence (SITE["ownership"]). Couples, engagements and weddings are advertised on dennyschulz.de only; desfoto shows single-person portraits, family and business work, and no couple imagery may be added back to src/images.json.
  • /fotografie/ is the umbrella term for the photographic work ("Shooting" is not a section label any more). Renaming a route means updating pages.ROUTES, nginx.conf (301 from the old URL) and scripts/build-site.py:ROUTE_OG; the build prunes the retired page from site/ automatically.
  • 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, analytics or tracking. The standalone /gtin/ tool is the only localStorage/Cache API exception: it keeps its last 20 generated entries in the browser and caches only its own offline app shell; it sends no tool data to a server. Its /gtin/impressum/ and /gtin/datenschutz/ pages are standalone, unlisted legal pages. The tool stays unlisted: no main-site navigation or page links, no sitemap entry, and noindex,nofollow; only the GTIN area links to its own legal pages, which never link back to the main site. The unlisted /gtin/aktualisieren/ page repairs old redirected app caches;
  • access_log off in nginx.conf — never enable request logging;
  • error_log /dev/null crit; in nginx.conf — never write an error-log file either;
  • fonts, scripts, styles and images are served from this origin only;
  • the GTIN service worker is scoped to /gtin/ and may not intercept other site routes or delete caches owned by another application;
  • 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 a mailto: 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.