vienalatina/README.md
Claude 72894b8907
Scaffold Hugo + Decap trilingual site from WordPress migration plan
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
2026-07-31 11:17:32 +00:00

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/...`).