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
102 lines
3.5 KiB
Markdown
102 lines
3.5 KiB
Markdown
# 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
|
|
|
|
```yaml
|
|
---
|
|
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
|
|
|
|
1. Publish `*.es.md` in Decap → one git commit, no waiting on DeepL.
|
|
2. Gitea webhook fires Woodpecker.
|
|
3. `scripts/translate.py` diffs the push, translates changed `*.es.md` via
|
|
DeepL (`tag_handling=html`, protected community terms), writes the
|
|
siblings, and pushes them back as a bot commit
|
|
(`translations@vienalatina.com`).
|
|
4. `hugo --minify` builds, `rsync --delete` deploys, Caddy serves.
|
|
|
|
Any DeepL error fails the pipeline visibly (red X, one-click retry) —
|
|
no half-translated sets ever ship.
|
|
|
|
## Local development
|
|
|
|
```sh
|
|
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-LD `BlogPosting`/`Blog` with
|
|
`inLanguage` — `themes/vienalatina/layouts/partials/seo-head.html`
|
|
- `robots.txt` AI-crawler allowlist — `static/robots.txt`
|
|
- `llms.txt` — generated at build time from `layouts/index.llms.txt`
|
|
- `sitemap.xml` — Hugo native, multilingual
|
|
|
|
## One-shot content migration
|
|
|
|
```sh
|
|
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/...`).
|