Merge URL-collision fixes
This commit is contained in:
commit
f8504f7a68
@ -42,7 +42,13 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
# Bundle Decap locally so the /admin page makes zero third-party requests.
|
# Bundle Decap locally so the /admin page makes zero third-party requests.
|
||||||
- wget -q -O static/admin/decap-cms.js https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js
|
- wget -q -O static/admin/decap-cms.js https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js
|
||||||
- hugo --minify
|
# --printPathWarnings makes Hugo report two pages resolving to one output
|
||||||
|
# path; --panicOnWarning turns that report into a failed build. Without
|
||||||
|
# both, Hugo writes both documents into the same index.html and the site
|
||||||
|
# ships a page that renders as the article followed by a second copy of
|
||||||
|
# the whole site — with nothing in the CMS, the pipeline or the logs
|
||||||
|
# saying a word about it. Better a red build than a corrupted page.
|
||||||
|
- hugo --minify --printPathWarnings --panicOnWarning
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
image: alpine:3.20
|
image: alpine:3.20
|
||||||
|
|||||||
12
config.yaml
12
config.yaml
@ -9,9 +9,17 @@ theme: "vienalatina"
|
|||||||
defaultContentLanguage: "es"
|
defaultContentLanguage: "es"
|
||||||
defaultContentLanguageInSubdir: false
|
defaultContentLanguageInSubdir: false
|
||||||
|
|
||||||
# Preserve WordPress slugs so external inbound links keep working.
|
# Date-scoped so two posts can share a title without fighting over one URL.
|
||||||
|
# `:slug` falls back to the title when front matter sets none, so a bare
|
||||||
|
# "/:slug/" mapped every post called "Hola mundo" onto /hola-mundo/; Hugo wrote
|
||||||
|
# both documents into that one index.html and the page rendered as two stacked
|
||||||
|
# copies of the site, with no error anywhere. Community blogs repeat titles
|
||||||
|
# constantly ("Resumen del mes"), so the date carries the uniqueness. The day is
|
||||||
|
# part of it because the two posts that exposed this were eleven days apart in
|
||||||
|
# the same month; only same-title-same-day now collides, and the build guard in
|
||||||
|
# .woodpecker.yml fails that loudly instead of shipping it.
|
||||||
permalinks:
|
permalinks:
|
||||||
post: "/:slug/"
|
post: "/:year/:month/:day/:slug/"
|
||||||
|
|
||||||
taxonomies:
|
taxonomies:
|
||||||
category: "categories"
|
category: "categories"
|
||||||
|
|||||||
@ -146,8 +146,15 @@ Then create the two OAuth apps and the bot user, all in the Gitea web UI:
|
|||||||
- Save the **Client ID** and **Client Secret** — needed in step 7.
|
- Save the **Client ID** and **Client Secret** — needed in step 7.
|
||||||
2. **Decap OAuth app:** same screen, second application
|
2. **Decap OAuth app:** same screen, second application
|
||||||
- Name: `decap-cms`
|
- Name: `decap-cms`
|
||||||
- Redirect URI: `https://vienalatina.com/admin/`
|
- Redirect URI: `https://vienalatina.com/admin/` (exactly, trailing slash
|
||||||
|
included — Gitea matches it literally)
|
||||||
|
- **Untick "Confidential Client".** Decap runs in the browser and
|
||||||
|
authenticates with PKCE; a confidential app makes Gitea demand a client
|
||||||
|
secret that a browser cannot keep, and the login fails *after* you
|
||||||
|
authorize, which makes it look like a Decap bug.
|
||||||
- Save the **Client ID** — it goes into `static/admin/config.yml` (step 9).
|
- Save the **Client ID** — it goes into `static/admin/config.yml` (step 9).
|
||||||
|
There is no secret to save, and the Client ID is not one either: it is
|
||||||
|
published in the site's JavaScript by design.
|
||||||
3. **Translations bot:** Site Administration → Identity & Access →
|
3. **Translations bot:** Site Administration → Identity & Access →
|
||||||
User Accounts → *Create User Account*
|
User Accounts → *Create User Account*
|
||||||
- Username: `translations`, email: `translations@vienalatina.com`,
|
- Username: `translations`, email: `translations@vienalatina.com`,
|
||||||
@ -225,6 +232,21 @@ On your working copy: edit `static/admin/config.yml`, replace
|
|||||||
step 6.2, commit, push to Gitea. (You can't log into `/admin` until the main
|
step 6.2, commit, push to Gitea. (You can't log into `/admin` until the main
|
||||||
domain is live — that's expected.)
|
domain is live — that's expected.)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd ~/vienalatina
|
||||||
|
sed -i 's/REPLACE_WITH_GITEA_OAUTH_CLIENT_ID/<client id>/' static/admin/config.yml
|
||||||
|
grep app_id static/admin/config.yml
|
||||||
|
git commit -am "Wire Decap to the Gitea OAuth app" && git push gitea main
|
||||||
|
```
|
||||||
|
|
||||||
|
This value is per-deployment: the placeholder is what belongs in the repo, so
|
||||||
|
leave it in place in any copy of this platform that is not this server.
|
||||||
|
|
||||||
|
If `/admin/` still shows *Client ID not registered* afterwards, the page is
|
||||||
|
serving a cached `config.yml` — hard-reload it. If it fails *after* the Gitea
|
||||||
|
authorize screen instead, the app was created as a confidential client; delete
|
||||||
|
it and recreate it with that box unticked.
|
||||||
|
|
||||||
## 10. Test the translation loop end-to-end
|
## 10. Test the translation loop end-to-end
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
# /etc/caddy/Caddyfile — copy this file there, then: sudo systemctl reload caddy
|
# /etc/caddy/Caddyfile — copy this file there, then: sudo systemctl reload caddy
|
||||||
#
|
#
|
||||||
# Caddy fetches and renews Let's Encrypt certificates automatically the first
|
# Caddy fetches and renews Let's Encrypt certificates automatically the first
|
||||||
# time a domain's DNS points at this server. git.* and ci.* work immediately;
|
# time a domain's DNS points at this server. Order matters on a cutover: move
|
||||||
# the vienalatina.com block stays commented until cutover day (Sunday evening),
|
# the A record here first, then reload Caddy — the ACME HTTP-01 challenge for
|
||||||
# because Caddy can only get its certificate once the A record points here.
|
# vienalatina.com only succeeds once the domain already resolves to this box.
|
||||||
|
|
||||||
git.vienalatina.com {
|
git.vienalatina.com {
|
||||||
reverse_proxy 127.0.0.1:3000
|
reverse_proxy 127.0.0.1:3000
|
||||||
@ -14,50 +14,50 @@ ci.vienalatina.com {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# UNCOMMENT EVERYTHING BELOW ON CUTOVER DAY (after flipping the A record)
|
# Live since cutover (A record for vienalatina.com points at this server).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# www.vienalatina.com {
|
www.vienalatina.com {
|
||||||
# redir https://vienalatina.com{uri} 301
|
redir https://vienalatina.com{uri} 301
|
||||||
# }
|
}
|
||||||
#
|
|
||||||
# vienalatina.com {
|
vienalatina.com {
|
||||||
# root * /var/www/vienalatina.com
|
root * /var/www/vienalatina.com
|
||||||
# encode zstd gzip
|
encode zstd gzip
|
||||||
# file_server
|
file_server
|
||||||
#
|
|
||||||
# # llms.txt is markdown (matches the old WP behaviour)
|
# llms.txt is markdown (matches the old WP behaviour)
|
||||||
# header /llms.txt Content-Type "text/markdown; charset=utf-8"
|
header /llms.txt Content-Type "text/markdown; charset=utf-8"
|
||||||
#
|
|
||||||
# # --- 301s for old WordPress URL patterns -----------------------------
|
# --- 301s for old WordPress URL patterns -----------------------------
|
||||||
#
|
|
||||||
# # /?p=123 style permalinks → home (add specific mappings as GSC reports them)
|
# /?p=123 style permalinks → home (add specific mappings as GSC reports them)
|
||||||
# @wp_query_permalink {
|
@wp_query_permalink {
|
||||||
# path /
|
path /
|
||||||
# query p=*
|
query p=*
|
||||||
# }
|
}
|
||||||
# redir @wp_query_permalink / 301
|
redir @wp_query_permalink / 301
|
||||||
#
|
|
||||||
# # WP category base was /categoria/… (Spanish slugs); Hugo uses /categories/…
|
# WP category base was /categoria/… (Spanish slugs); Hugo uses /categories/…
|
||||||
# @old_category path_regexp oldcat ^/categoria/(.*)$
|
@old_category path_regexp oldcat ^/categoria/(.*)$
|
||||||
# redir @old_category /categories/{re.oldcat.1} 301
|
redir @old_category /categories/{re.oldcat.1} 301
|
||||||
#
|
|
||||||
# # WP author archives have no Hugo equivalent → home
|
# WP author archives have no Hugo equivalent → home
|
||||||
# @old_author path /autor/*
|
@old_author path /autor/*
|
||||||
# redir @old_author / 301
|
redir @old_author / 301
|
||||||
#
|
|
||||||
# # Old WP media library URLs → migrated uploads folder
|
# Old WP media library URLs → migrated uploads folder
|
||||||
# @old_uploads path_regexp oldup ^/wp-content/uploads/(?:\d{4}/\d{2}/)?(.*)$
|
@old_uploads path_regexp oldup ^/wp-content/uploads/(?:\d{4}/\d{2}/)?(.*)$
|
||||||
# redir @old_uploads /uploads/{re.oldup.1} 301
|
redir @old_uploads /uploads/{re.oldup.1} 301
|
||||||
#
|
|
||||||
# # Anything else that starts with /wp- doesn't exist anymore
|
# Anything else that starts with /wp- doesn't exist anymore
|
||||||
# @wp_leftovers path /wp-admin/* /wp-login.php /wp-json/* /xmlrpc.php
|
@wp_leftovers path /wp-admin/* /wp-login.php /wp-json/* /xmlrpc.php
|
||||||
# redir @wp_leftovers / 301
|
redir @wp_leftovers / 301
|
||||||
#
|
|
||||||
# # Custom 404 falls back to Hugo's 404 page
|
# Custom 404 falls back to Hugo's 404 page
|
||||||
# handle_errors {
|
handle_errors {
|
||||||
# @404 expression {err.status_code} == 404
|
@404 expression {err.status_code} == 404
|
||||||
# rewrite @404 /404.html
|
rewrite @404 /404.html
|
||||||
# file_server
|
file_server
|
||||||
# }
|
}
|
||||||
# }
|
}
|
||||||
|
|||||||
@ -14,6 +14,15 @@ services:
|
|||||||
- GITEA__server__SSH_DOMAIN=git.vienalatina.com
|
- GITEA__server__SSH_DOMAIN=git.vienalatina.com
|
||||||
- GITEA__service__DISABLE_REGISTRATION=true
|
- GITEA__service__DISABLE_REGISTRATION=true
|
||||||
- GITEA__webhook__ALLOWED_HOST_LIST=ci.vienalatina.com
|
- GITEA__webhook__ALLOWED_HOST_LIST=ci.vienalatina.com
|
||||||
|
# Decap is served from vienalatina.com but exchanges its OAuth code for a
|
||||||
|
# token by calling git.vienalatina.com from the browser — a cross-origin
|
||||||
|
# request. Without this, Gitea returns no Access-Control-Allow-Origin, the
|
||||||
|
# browser drops the response, and Decap reports "TypeError: Failed to
|
||||||
|
# fetch" right after you authorize, which reads like a Decap bug.
|
||||||
|
- GITEA__cors__ENABLED=true
|
||||||
|
- GITEA__cors__ALLOW_DOMAIN=https://vienalatina.com
|
||||||
|
- GITEA__cors__METHODS=GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
|
||||||
|
- GITEA__cors__HEADERS=Content-Type,User-Agent,Authorization
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
|||||||
@ -78,6 +78,14 @@ def split_lang(path: Path) -> tuple[str, str] | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Decap resolves a filename clash by appending a counter, turning
|
||||||
|
# `hola-mundo.es.md` into `hola-mundo.es-1.md`. That name no longer ends in a
|
||||||
|
# language, so Hugo stops pairing it with its siblings and split_lang() returns
|
||||||
|
# None: the post publishes in Spanish and is never translated, with nothing
|
||||||
|
# anywhere reporting it. Recognising the shape lets us fail loudly instead.
|
||||||
|
CLASH_SUFFIX = re.compile(r"\.(?:%s)-\d+$" % "|".join(re.escape(l) for l in SITE_LANGS))
|
||||||
|
|
||||||
|
|
||||||
def split_frontmatter(text: str) -> tuple[dict, str]:
|
def split_frontmatter(text: str) -> tuple[dict, str]:
|
||||||
match = re.match(r"\A---\n(.*?)\n---\n?(.*)\Z", text, re.DOTALL)
|
match = re.match(r"\A---\n(.*?)\n---\n?(.*)\Z", text, re.DOTALL)
|
||||||
if not match:
|
if not match:
|
||||||
@ -130,6 +138,14 @@ def authored_sources(paths: list[Path]) -> list[tuple[Path, str, str]]:
|
|||||||
for path in paths:
|
for path in paths:
|
||||||
parsed = split_lang(path)
|
parsed = split_lang(path)
|
||||||
if not parsed:
|
if not parsed:
|
||||||
|
rel = path.relative_to(REPO_ROOT)
|
||||||
|
if CLASH_SUFFIX.search(path.name[: -len(".md")] if path.suffix == ".md" else ""):
|
||||||
|
raise SystemExit(
|
||||||
|
f"{rel}: filename ends in a clash counter, so it is neither a "
|
||||||
|
f"translation source nor a sibling — it would publish untranslated.\n"
|
||||||
|
f"Rename it to <something-unique>.<lang>.md (the CMS produced this "
|
||||||
|
f"because another post already claimed the name)."
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
basename, lang = parsed
|
basename, lang = parsed
|
||||||
fm = read_frontmatter(path)
|
fm = read_frontmatter(path)
|
||||||
|
|||||||
@ -22,7 +22,12 @@ collections:
|
|||||||
create: true
|
create: true
|
||||||
extension: md
|
extension: md
|
||||||
format: yaml-frontmatter
|
format: yaml-frontmatter
|
||||||
slug: "{{slug}}.es"
|
# Date-prefixed so a repeated title does not collide on the filename. When
|
||||||
|
# it does collide, Decap appends "-1" and writes `hola-mundo.es-1.md` — a
|
||||||
|
# name whose suffix is no longer `.es`, so Hugo stops seeing a Spanish
|
||||||
|
# sibling and translate.py's split_lang() skips the file entirely: the post
|
||||||
|
# publishes but is never translated, with nothing reported anywhere.
|
||||||
|
slug: "{{year}}-{{month}}-{{day}}-{{slug}}.es"
|
||||||
filter: { field: lang, value: es }
|
filter: { field: lang, value: es }
|
||||||
fields:
|
fields:
|
||||||
- { name: title, label: "Título", widget: string }
|
- { name: title, label: "Título", widget: string }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user