Commit Graph

71 Commits

Author SHA1 Message Date
ddf250b61a Merge branch 'claude/relaxed-faraday-h4zd09' of https://github.com/pablovolenski/vienalatina
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-25 18:55:24 +00:00
Claude
bf7ff3025a
Say which remote to pull from, because git will not
The server's main tracks gitea/main while new work lands on a branch on
GitHub, so a bare `git pull` asks Gitea, finds it level, and answers
"Already up to date." That sentence is true about the wrong remote and
reads exactly like there is nothing to fetch — which is how the last
deploy stalled on a script that had never arrived.

Step 8 now names a `github` remote beside `gitea`, and the update recipe
in 11.3 pulls from it by name with --no-edit, pushes to gitea, then
rebuilds. 11.3 previously said plain `git pull`, as though the server
tracked GitHub.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-25 18:54:00 +00:00
Claude
ed6b568d60
Hand the mail settings to the container, and rebuild on deploy
Two silent failures sitting between Phase A and a working invitation.

The compose file never passed MAIL_* through. Compose does not give a
container the contents of .env — it only substitutes into the compose
file — so the settings could be filled in correctly and read by nobody,
with the app reporting mail as unconfigured and the values sitting right
there on disk. test_deployment.py now fails the build whenever
.env.example and docker-compose.yml drift apart, which is how this
happened and how it would happen again.

The app's code is baked into the image (COPY apps /srv/apps), so a pull
followed by --force-recreate runs the old code and says nothing. Added
scripts/deploy-board.sh: rebuild, sync the compose file, restart, print
the log, and name any setting present in .env.example and missing from
the live .env. It never touches .env itself.

Also: "no mail server configured" is now told apart from "the mail
server refused". They want different things done about them, and the
first one sent an admin looking for an SMTP error that never existed.
The new-member form says so before it is filled in rather than after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-25 18:42:39 +00:00
e55ea94619 Merge branch 'claude/relaxed-faraday-h4zd09' of https://github.com/pablovolenski/vienalatina
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-25 18:24:08 +00:00
Claude
e95c732c6f
Let members get a password of their own
Until now an admin created an account, the password appeared once on screen,
and that was the only copy. Gitea's Forgot password answers "Account recovery
is disabled because no email is set up", because this server has never been
able to send mail. Every route a member had to a password of their own was
closed, which made the members area unusable by anyone not standing next to the
owner.

Now: the admin enters a username and an email, the server creates the Gitea
account with a random password nobody ever sees — the admin included — and the
member is emailed a link to a Viena Latina page where they choose their own.
The same mechanism gives "¿Olvidaste tu contraseña?" that works, replacing
Gitea's dead page. Nobody leaves the site for either, which is possible because
PATCH /admin/users/{username} accepts a password.

A link is enough to take an account, so it is treated as a credential: single
use, short-lived, stored only as a SHA-256, and invalidated when a newer one is
issued so an older email stops working. SHA-256 rather than a password hash
because these are 32 random bytes, not something a person chose — there is no
dictionary to slow down. Recovery answers identically for a known and an
unknown address, or the form becomes a way to enumerate members one address at
a time, and stops after three tries so it cannot be used to mail-bomb somebody
using this server's reputation.

Rejecting a password deliberately does not spend the token. A typo must not
lock somebody out of an account they have never reached.

If the mail fails the admin is shown the link instead. The account exists
either way, so the difference is between a delayed invitation and a person who
simply never gets in.

Found while testing: the rate limit did not work at all. created_at is written
by SQLite as "2026-09-25 15:00:00" and I compared it against Python's
"2026-09-25T15:00:00+00:00"; a space sorts before T, so every row looked older
than any threshold and the limit silently never fired. It is now compared
inside SQLite, where the format and the clock are the same one.

Also drops two tabs from the sign-in page: OpenID, which nobody here will use,
and Register, which contradicted DISABLE_REGISTRATION and invited people to try
something the server then refused.

Verified: 126 checks. Tokens are hashed at rest, work once, expire, die when
reissued, and are refused for a suspended member; a short password and a Gitea
rejection both leave the link usable; recovery is indistinguishable for known
and unknown addresses and stops at three; creating a member emails them and
shows no password; a failed send surfaces the link.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-25 17:49:39 +00:00
Claude
3bd086ab0e
Say accounts cannot be created before the form is filled in
Running without GITEA_ADMIN_TOKEN is the safer configuration and is documented
as such, but the member form did not know: it offered "Crear también su cuenta"
ticked by default and reported "Falta GITEA_ADMIN_TOKEN" only on submit, after
three fields had been filled in.

That is the shape of failure this project has lost the most time to — something
that cannot happen, going unsaid until someone has relied on it. The form now
reads the config when it renders and says so, with a link to Gitea's create-user
page.

The checkbox is disabled rather than hidden, because "you cannot do this here"
is more use than an option that quietly is not there. The refusal itself stays
in the handler: a disabled input is a courtesy, and a hand-crafted POST still
meets the same error.

Verified: 104 checks. The form states the limit with no token and is unchanged
with one, and submitting create_account anyway still creates no member.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-25 17:17:41 +00:00
8e09c461d0 Merge branch 'claude/relaxed-faraday-h4zd09' of https://github.com/pablovolenski/vienalatina
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-25 17:03:48 +00:00
Claude
acf3f745ff
Read Gitea's base theme over HTTP, not from its filesystem
The install script read /app/gitea/public/assets/css/theme-gitea-light.css out
of the container. The official image compiles its static assets into the gitea
binary, so that path does not exist and the script could never have worked —
it would have failed on its own "could not read Gitea's own theme" check,
which at least pointed somewhere, but at the wrong thing entirely.

Fetching the file from the running server instead works whether the assets are
embedded or on disk, and has the better property besides: what the server
serves is by definition the version that is running, which is the whole reason
the base is not kept in this repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-25 16:32:43 +00:00
bfc10c5a96 Merge branch 'claude/relaxed-faraday-h4zd09' of https://github.com/pablovolenski/vienalatina
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-25 16:28:57 +00:00
Claude
724dada3f4
Brand Gitea's auth screens, and stop Salir overstating itself
Two halves of the same complaint: the journey into the members area does not
feel like one platform.

Members sign in to /comunidad/ through Gitea, so Gitea's sign-in form and
authorize dialog are part of everyone's journey, not just of anyone who opens a
repository. That is what the earlier "is Gitea only for admins?" question got
wrong. Unthemed they are two dark screens in the middle of a cream site.

Unlike Decap, Gitea supports being themed, so this needs no forking and nothing
a release can silently undo. The repository holds only the colour overrides;
scripts/gitea-theme.sh concatenates them onto the light theme it reads out of
the running container. Keeping somebody else's stylesheet in here would go
stale and turn every Gitea upgrade into a merge — the script is re-run instead,
which is one line in the upgrade notes. Getting a variable name wrong leaves a
corner grey rather than breaking a page, which is the failure mode worth having
when the names belong to someone else's project.

The logo is the site's wordmark, as live text in the same font stack rather than
traced to paths: the site loads no webfont at all, deliberately, so visitors
already see it in whatever sans-serif their machine substitutes. Paths would
render a Montserrat most people never see on the site itself.

Salir was the sharper problem. It cleared this session and the stored token, and
then said "Sesión cerrada" — while the Gitea session in the same browser stayed
open and Gitea still remembered the authorisation, so one click signed the next
person straight back in with no password. On a laptop shared around an
association, that button was lying.

It cannot be fixed from here. Gitea's logout has been POST-only since 1.11.2, so
a link cannot trigger it and a cross-site POST needs a CSRF token this app does
not have; prompt=login is undocumented in every released version of Gitea's
OAuth2 provider, and a security control should not rest on that. So the logout
page now says exactly what is closed and what is not, and links to the one place
that finishes it. Being honest about a limit beats a reassuring message that is
false.

Verified: 101 checks, two of them new — logout warns rather than redirecting,
and leaves no token the server could still act with. The theme itself is
visual and has to be looked at; docs/server-setup.md §12 says where.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-25 16:28:10 +00:00
0893c713e4 Merge branch 'claude/relaxed-faraday-h4zd09' of https://github.com/pablovolenski/vienalatina
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-25 16:12:30 +00:00
Claude
635372d7a2
Write the site from the members area instead of Decap
Decap looks like a different product because it cannot be made to look like
this one: its maintainer's answer is override the CSS and accept that class
names move between releases, or fork it and carry that forever. Neither is
worth building on, so editing moves into the app that already has the site's
design and this project's tests.

Posts are files in git, so the editor is a form that commits a file through
Gitea's contents API. No working copy, no queue, no new state, and no way for
the pipeline to tell which editor wrote the file — which is what makes running
both at once safe. Decap keeps working; nothing here removes it, and the
fallback if something is missing is switching tabs.

Filenames and frontmatter are not this module's to invent. They are the
contract scripts/translate.py reads: `<basename>.es.md` is what split_lang()
parses, the date prefix is what keeps two posts with one title off a single
URL, and the absence of `translated_from` is what marks a file as something a
person wrote. Several tests import translate.py and run its own parser over
what the editor produced, because a file it cannot parse publishes in Spanish
and is never translated, with nothing reported anywhere.

Commits carry the writer's own Gitea account rather than a bot's, so history
says who wrote each post and Gitea's permissions apply unchanged. That needs
their access token, which lives in the database and never in a cookie: Flask
signs cookies but does not encrypt them, and a token is enough to commit as its
owner. Gitea expires tokens after about an hour, so they refresh ahead of
expiry and retry once on rejection — without that, saving would start failing
partway through an afternoon for no reason the writer could see.

Publishing is admin-only. Posting to the internal board and publishing to the
public site are different permissions, and the second is the larger grant.

Listing caches frontmatter against the git blob sha, because the contents API
returns names without bodies: that turns one request per post on every page
load into one request in total, and needs no invalidation, since a sha changes
only when the file does.

Verified: 97 checks. The filename translate.py parses, an authored source that
is_generated() rejects, the freeze toggle round-tripping, a stale sha refused
with the other edit intact, six filename shapes that must 404 including a
generated sibling, plain members refused, an SVG rejected as an image, uploads
not colliding, the cache reading each file once and refreshing when it changes,
and a preview that renders markdown, escapes script tags and publishes nothing.
Smoke-tested live: routes register and gate correctly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-25 15:07:31 +00:00
16e4905dcb Merge branch 'claude/relaxed-faraday-h4zd09' of https://github.com/pablovolenski/vienalatina
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 11:01:53 +00:00
Claude
6ac814475e
Add a members area: roles and an internal board
Everything on this site so far has been a file built from git. This is the
first component that runs code to answer a request and the first whose data
git does not hold, so the trade is stated in the README and the backup script
is not optional.

Roles are owner, admin and user. The owner is seeded once from BOARD_OWNER and
cannot be seeded again, because otherwise editing a compose file would be a
quieter way to take the top role than asking for it; ownership moves only by
transfer, inside the app. There is exactly one owner and a partial unique index
enforces it, so the invariant holds even when a handler is wrong. The owner is
beyond suspension and demotion by everyone, themselves included. Only the owner
makes admins; admins make users.

Sign-in goes through Gitea as a confidential OAuth client — the opposite of
Decap, which has to be public because it runs in the browser. The rule the
whole thing rests on is that a Gitea account is not a membership: entry needs
an active row in `members`, or every account on the instance is a member,
starting with the translations bot.

Admins can delete any post; nobody can edit anyone else's, admins included.
Taking a post down is visible to its author. Quietly rewriting it is not, and
an admin who could do that could leave a sentence attributed to someone who
never wrote it. The plan said admins could do both; this is the one place the
implementation departs from it.

Markdown renders with raw HTML disabled, which is the entire XSS defence and
the reason there is no sanitiser: the renderer emits only its own tags and
escapes the rest. The CSP carries no 'unsafe-inline', which makes an inline
onsubmit silently inert rather than broken, so the confirmation dialogs live in
a static file and a test fails any template that grows an inline handler.

GDPR is in scope rather than deferred: erasure removes the member row and moves
their authorship to a tombstone so the conversations around them still read,
and any member can download their own writing.

Verified: 63 checks pass, covering the membership gate, every role predicate, a
direct insert of a second owner being refused by the index, atomic ownership
transfer, CSRF, an offsite login redirect, script tags rendering as text, soft
deletes leaving both listings and exports, and the member screens rendering for
each role. Smoke-tested live: headers, both static assets, and the bare
/comunidad redirect that the Caddy matcher has to cover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-22 10:57:18 +00:00
2733bf2763 Update Página “acerca.es”
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 10:49:33 +00:00
1ebb58c8ba translate: update 1 generated siblings [skip-translate]
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 10:49:06 +00:00
6066f1e330 Update Página “acerca.es”
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 10:48:50 +00:00
a85d3b08f5 translate: remove 2 orphaned by a deleted source [skip-translate]
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 10:01:48 +00:00
bbbc1d56da Delete Artículo “2026-09-22-hola-mundo-1.es”
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 10:01:42 +00:00
359784b1b4 translate: update 2 generated siblings [skip-translate]
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 10:00:13 +00:00
5d23984542 Create Artículo “2026-09-22-la-inteligencia-de-las-plantas.es”
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 10:00:01 +00:00
c95b27d3f2 translate: update 2 generated siblings [skip-translate]
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 09:57:16 +00:00
2e778ced18 Create Artículo “2026-09-22-la-pera-en-el-escritorio.es”
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 09:57:04 +00:00
e4c56f520f translate: remove 4 orphaned by a deleted source [skip-translate]
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 09:50:49 +00:00
3cb2c86217 Merge branch 'claude/relaxed-faraday-h4zd09' of https://github.com/pablovolenski/vienalatina
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 09:50:36 +00:00
Claude
da8cc784be
Remove generated siblings when their source is deleted
Deleting a post in Decap removes one file: the Spanish original. The German
and Portuguese versions were written by this script, so nothing else deletes
them, and they stayed on the site as posts in a language with no original —
/de/ and /pt-br/ kept listing articles that no longer exist in Spanish.

The pipeline had no concept of removal at all. changed_markdown() filters the
push diff to A and M, so a deletion never reached the translate step; a push
that only deletes posts arrived with no sources and returned early, which is
precisely the case that leaves siblings stranded.

Siblings are now reaped by scanning the content tree for a translated_from
whose source file is gone, which also clears debris from earlier runs and from
renames. A frozen sibling is reported rather than deleted: someone edited that
translation by hand, and throwing the work away on an inference is worse than
leaving one stale page until they remove it themselves. The reaper runs even
when nothing was translated, and commit_and_push stages with --all so a
vanished path commits as a deletion.

Verified: reaps both siblings of a deleted source, keeps siblings whose source
is alive, keeps a frozen orphan, never touches a human-authored original.
16/16 pipeline and 15/15 markdown checks still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-22 09:49:20 +00:00
fd29bf93ee translate: update generated siblings [skip-translate]
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 09:44:05 +00:00
28e273b305 Create Artículo “2026-09-22-hola-mundo-1.es”
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 09:43:54 +00:00
664de46525 Delete Artículo “2026-09-21-hola-mundo.es”
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 09:42:10 +00:00
c74de36bf1 Merge branch 'main' of https://git.vienalatina.com/pablo/vienalatina
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 09:39:16 +00:00
177dcb40e9 Backfill translations for the CMS post [skip-translate] 2026-09-22 09:35:52 +00:00
7d12a41b48 Merge rename-detection fix 2026-09-22 09:35:23 +00:00
84f3becbd1 translate: update generated siblings [skip-translate]
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 09:31:26 +00:00
4a0ac3697d Update Página “contacto.es”
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 09:31:15 +00:00
Claude
3d41e7f337
Notice a renamed post so it gets translated
changed_markdown() filtered the push diff to A and M. Git classifies a rename
as R, so the file renamed last night never reached the translate step: the post
published in Spanish alone, and because the language switcher only renders when
.IsTranslated is true, its page shows no ES/DE/PT links at all.

--no-renames makes git report the same change as a delete plus an add, and the
add is an ordinary translation source. Verified against a real repository: the
old filter returned nothing for a rename, the new one returns the new path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-22 09:28:23 +00:00
c994a86f41 Delete Artículo “hola-mundo.es”
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 09:25:11 +00:00
5e9e773cd0 Rename the CMS post to a proper Spanish source filename
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-22 08:44:00 +00:00
f8504f7a68 Merge URL-collision fixes 2026-09-22 08:43:36 +00:00
Claude
90c22cf02c
Scope post URLs to the day, not the month
Month granularity would not have separated the two posts that exposed this
bug: they are dated 18 and 21 September 2026, so both still resolved to
/2026/09/hola-mundo/ and the page would have stayed broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-22 07:50:51 +00:00
Claude
67706ee36e
Stop two posts with the same title from sharing one URL
Publishing a second "Hola mundo" through the CMS broke the first one's page:
it rendered the article, then a second copy of the entire site. Three silent
failures lined up.

Decap could not write hola-mundo.es.md twice, so it wrote hola-mundo.es-1.md.
That suffix is not a language, so Hugo stopped treating the file as a Spanish
sibling and translate.py's split_lang() skipped it — the post went live in
Spanish alone, and no German or Portuguese was ever generated. Meanwhile
permalinks used "/:slug/", and :slug falls back to the title, so both files
claimed /hola-mundo/; Hugo wrote both documents into that one index.html.
Nothing failed. The pipeline was green throughout.

Each layer now refuses its part: post permalinks carry the year and month, the
CMS prefixes new filenames with the date, and translate.py aborts on a name
ending in a clash counter rather than quietly declining to translate it. The
build also runs with --printPathWarnings --panicOnWarning, so any future pair
of pages targeting one path fails the build instead of corrupting the output.

Existing post URLs change shape (/hola-mundo/ becomes /2026/09/hola-mundo/).
That costs nothing today, with one real post and no inbound links, and gets
expensive to change later.

Verified: 16/16 pipeline checks and 15/15 markdown checks still pass, the
clash-counter name aborts with the rename instruction, and an ordinary
filename still parses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-22 07:49:46 +00:00
96f4c53990 Create Artículo “hola-mundo.es-1”
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-21 08:20:20 +00:00
Claude
b69e9d6f81
Let Gitea answer Decap's cross-origin token request
Decap is served from vienalatina.com and posts its OAuth code to
git.vienalatina.com/login/oauth/access_token from the browser. Gitea disables
CORS by default, so the browser discarded the response and Decap surfaced it
as "TypeError: Failed to fetch" after a successful authorize — the login looks
broken at the last step, with nothing wrong on either side individually.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-18 14:17:44 +00:00
1d267815d0 Update static/admin/config.yml
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-18 14:13:52 +00:00
06955e3bf5 Set the real Decap client ID; fix and freeze the German Über uns title
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-18 14:07:21 +00:00
5a75edd66f Wire Decap to the Gitea OAuth app
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-18 14:00:37 +00:00
Claude
6890ff4153
Document the Decap OAuth app as a public PKCE client
Gitea ticks "Confidential Client" by default, which makes it demand a client
secret. Decap runs in the browser and authenticates with PKCE, so there is
nowhere to keep one — the login then fails after the authorize screen, which
reads like a Decap bug rather than a registration mistake.

Also spells out step 9 as commands, notes that the Client ID is published in
the site's JavaScript by design (so committing it is fine, but the placeholder
is what belongs in a resold copy), and records the two failure modes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-18 13:57:17 +00:00
d0c2acb4d5 Backfill page translations [skip-translate]
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-18 13:42:04 +00:00
Claude
b96920c6bc
Enable the vienalatina.com site block in the Caddyfile
The block was parked behind a comment marker until the domain's A record
pointed at this server. It does now, so the checked-in config should match
what is actually serving; leaving it commented meant a redeploy from the
repo would silently take the site down.

Also corrects the header note to say why the order is DNS-then-reload: the
ACME HTTP-01 challenge for vienalatina.com can only pass once the domain
already resolves here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
2026-09-18 13:32:42 +00:00
438d09134e translate: update generated siblings [skip-translate]
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-18 13:25:41 +00:00
7f7f535bda Reset content; start from a single post
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-18 13:25:23 +00:00