vienalatina/README.md
Claude 66903016c7
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Add infra files and step-by-step server setup guide
- infra/caddy/Caddyfile: git.* and ci.* proxies live now; main-site block
  (with WP 301 redirects) commented until cutover day
- infra/gitea and infra/woodpecker: Docker Compose bound to localhost
  behind Caddy, with .env.example for the Woodpecker OAuth credentials
- docs/server-setup.md: every command from ordering the CX22 through the
  end-to-end translation test, plus the cutover-day checklist

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017z2rT1oN7vMS4ggo23n5WG
2026-07-31 11:36:43 +00:00

107 lines
3.6 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.
## Server setup
Full command-by-command walkthrough: **[docs/server-setup.md](docs/server-setup.md)**.
The Caddyfile and Docker Compose files it uses live in [infra/](infra/).
## 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/...`).