Static replacement for the WordPress + Polylang + synchronous DeepL stack: - Hugo config: es (default) / de / pt-br, siblings paired by filename basename, WP slugs preserved via /:slug/ permalinks - Theme: CSS ported verbatim from the WP theme; header/footer/lang-switcher and article templates ported from their PHP counterparts (no Google Fonts — zero third-party requests) - seo-head partial: hreflang + x-default, og:locale(:alternate), JSON-LD BlogPosting/Blog with inLanguage (ported from theme/inc/seo.php) - robots.txt AI-crawler allowlist + build-time llms.txt (ported from theme/inc/robots-and-llms.php) - scripts/translate.py: async DeepL step (tag_handling=html, protected community terms, manual_translation freeze, fail-loud, bot commits) - scripts/wp-to-hugo.py: one-shot WP REST → Hugo content converter - Decap CMS admin with Gitea backend, locally-bundled JS - .woodpecker.yml: translate → build → deploy (rsync to Caddy root) - Sample trilingual welcome post + acerca/contacto pages Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017z2rT1oN7vMS4ggo23n5WG
3.5 KiB
Viena Latina
Static, self-hosted, trilingual blog for the Latin American community in Vienna — Hugo + Decap CMS + Gitea + Woodpecker CI on a single Hetzner CX22 (Nuremberg, DE). Replaces the previous WordPress + Polylang + synchronous DeepL stack.
Pablo ──► Decap CMS (/admin) ──commit──► Gitea ──webhook──► Woodpecker CI
│
translate (DeepL, async) ─┤
build (hugo) ─┤
deploy (rsync) ─┘
▼
Caddy 2 serves /var/www/vienalatina.com
Languages
Spanish is the authoring language; German (/de/) and Brazilian Portuguese
(/pt-br/) siblings are generated by CI as reviewable git commits. Siblings
pair by filename basename:
content/post/mi-articulo.es.md ← authored in Decap
content/post/mi-articulo.de.md ← written by scripts/translate.py
content/post/mi-articulo.pt-br.md ← written by scripts/translate.py
Set manual_translation: true in a sibling's frontmatter to freeze it —
CI will never overwrite it again.
Frontmatter contract
---
title: "Mi artículo"
date: 2026-07-31
lang: es
manual_translation: false # set true on *.de.md/*.pt-br.md to freeze
categories: [Gastronomía]
---
Translation flow
- Publish
*.es.mdin Decap → one git commit, no waiting on DeepL. - Gitea webhook fires Woodpecker.
scripts/translate.pydiffs the push, translates changed*.es.mdvia DeepL (tag_handling=html, protected community terms), writes the siblings, and pushes them back as a bot commit (translations@vienalatina.com).hugo --minifybuilds,rsync --deletedeploys, Caddy serves.
Any DeepL error fails the pipeline visibly (red X, one-click retry) — no half-translated sets ever ship.
Local development
hugo server # http://localhost:1313
CI secrets (Woodpecker → repo settings → secrets)
| Secret | Value |
|---|---|
deepl_api_key |
DeepL free-tier key (500k chars/mo) |
gitea_push_token |
Gitea token for the translations bot user, repo write access |
The repo must be marked trusted in Woodpecker so the deploy step can
mount /var/www/vienalatina.com.
Decap CMS
static/admin/config.yml uses the Gitea backend. Register an OAuth app in
Gitea admin (redirect URI https://vienalatina.com/admin/) and put its
client ID in app_id. The Decap JS bundle is downloaded at build time into
static/admin/decap-cms.js (gitignored) — zero third-party requests at
runtime.
GEO/SEO surfaces
hreflang+og:locale(:alternate)+ JSON-LDBlogPosting/BlogwithinLanguage—themes/vienalatina/layouts/partials/seo-head.htmlrobots.txtAI-crawler allowlist —static/robots.txtllms.txt— generated at build time fromlayouts/index.llms.txtsitemap.xml— Hugo native, multilingual
One-shot content migration
pip install requests html2text
python scripts/wp-to-hugo.py https://<old-wp-site>
Converts every WP post (with Polylang siblings) to
content/post/<slug>.<lang>.md, downloads images into static/uploads/,
and preserves existing slugs. Spot-check ~10 articles before committing.
Add Caddy 301s for WP URL patterns that don't map cleanly
(?p=123, /categoria/... → /categories/...).