Deleting a pond already had a strict prompt — typing the pond name, stricter
than a confirm dialog. That was never the gap. The gap is that the person who
deletes it loses access the moment they do: the pond leaves their view, only a
Site Admin can bring it back, and the export is no longer reachable for them.
So the archive is offered INSIDE the deletion flow, before the button.
What it contains, and why it is not the existing export:
- Every page the requester may read, as Markdown, as before.
- **Every attachment of the pond**, not only the embedded ones. An
attachment nobody put on a page would otherwise vanish unnoticed — which
is the whole reason this issue exists.
- `manifest.json`: pond settings (EFFECTIVE, defaults filled in — a
preservation format must not require its reader to know Dorfteich's
defaults), labels, the page hierarchy and sort keys, comments, and
attachment metadata including the #199 hash so a reader can verify bytes.
It extends the #210 manifest rather than adding a second descriptor, and
carries an explicit `formatVersion`.
- `README.txt`, because the manifest is for machines: whoever unpacks a
folder of Markdown a year from now must not believe they hold a one-click
restore.
Decisions worth naming:
- **"Complete" describes the RESULT, not the route.** A pond admin who may
read every page gets `complete: true`; only an archive that actually
leaves pages out is incomplete. The Site-Admin route skips the read filter
(an archive taken before an irreversible purge must not depend on which
ponds the operator happens to be a member of) — those are two different
questions and the first version of this conflated them.
- **The omission is named before the download**, with its number, in the UI
and in the manifest. An archive silently missing content is worse than no
archive, because it ends the search.
- **Not downloading stays allowed.** A pond of test pages should not require
one, and the server cannot tell whether a file arrived anyway — so the
finality is stated in text instead of enforced.
- **A plain link, not fetch-into-a-blob.** The api streams the ZIP; buffering
a whole pond in the tab to draw a progress bar would trade memory for
cosmetics. The browser reports progress and completion; what it cannot say
— that the archive is being BUILT — is announced in a live region.
- Read trail unchanged in kind (ADR 0023): one `export` event per classified
page before any classified byte enters the stream. Attachments never travel
without their page, so the same events cover them.
- New audit action `pond.archived` (catalogue v1.7) with page and attachment
counts, omitted pages, and completeness.
Format documented in `docs/architecture/pond-archive-format.md`, including
what is deliberately NOT in it (history, permissions, trash).
Verified by hand, not only asserted: a real pond's archive downloaded and
unpacked — README, manifest, three page files, the media file; the manifest's
effective settings, per-page classification, the VS-NfD frontmatter and
marking preserved in the classified page's Markdown, and the attachment's
sha256 present. Plus six api tests (including that an unembedded attachment
travels and that a Site Admin gets a complete archive without membership) and
the a11y pack 11/11 in both schemes, which now also scans the pond settings
screen.
Not done, because there is nothing to attach it to: the Site Admin's purge
dialog (#193) exists only as an api endpoint — there is no pond-trash UI in
the web app. The api half is here and tested, so it becomes a link when that
screen is built.
`apps/web/index.html` carries a hard `lang="en"`. The app corrects it at
runtime (#163), but nginx answers every SPA route with that same file, so a
crawler or a no-JS visit — `/public/...` on prod is exactly that — saw `en`
for German content, permanently. WCAG 3.1.1 is about the delivered document,
not the one JavaScript later fixes.
nginx-only, no backend involved: a `map` on `Accept-Language` and a
`sub_filter` in the index.html path. Only the FIRST tag decides, which is
what "the browser's preferred language" means and mirrors #163 — `de-CH`
counts as German, `en-US,de` does not.
`Vary: Accept-Language` is new. The response now genuinely depends on a
request header, and without it a shared cache could hand one language's copy
to the other. Everything else in the location is untouched: same CSP, same
`Cache-Control: no-cache`, same `nosniff`.
The known limit is documented in the config rather than worked around: nginx
cannot read `instance.defaultLocale`, so an unlisted or absent
Accept-Language yields `en` even on a German instance. For public content
that is not the authoritative rendering anyway — the api's server shell
(`/api/v1/public/...`) already renders those with the instance locale.
Verified against a real nginx 1.27 (the image the stage runs) with the
config mounted as-is: `de-DE,de;q=0.9,en;q=0.8` and `de` yield `lang="de"`;
`en-US,en;q=0.9`, `en-US,en;q=0.9,de;q=0.8`, `fr-FR,fr` and a request with no
header yield `lang="en"`; an SPA route (`/public/teich/seite`) negotiates the
same way; the gzipped response is rewritten too, and a JavaScript asset comes
through byte-identical.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.