Go to file
Pablo Volenski 694582814f
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
test: pipeline round-trip
2026-09-14 15:02:45 +00:00
content test: pipeline round-trip 2026-09-14 15:02:45 +00:00
docs Add infra files and step-by-step server setup guide 2026-07-31 11:36:43 +00:00
infra Add infra files and step-by-step server setup guide 2026-07-31 11:36:43 +00:00
scripts Scaffold Hugo + Decap trilingual site from WordPress migration plan 2026-07-31 11:17:32 +00:00
static Scaffold Hugo + Decap trilingual site from WordPress migration plan 2026-07-31 11:17:32 +00:00
themes/vienalatina Scaffold Hugo + Decap trilingual site from WordPress migration plan 2026-07-31 11:17:32 +00:00
.gitignore Scaffold Hugo + Decap trilingual site from WordPress migration plan 2026-07-31 11:17:32 +00:00
.woodpecker.yml Install git in the translate step's container 2026-09-14 14:45:51 +00:00
config.yaml Scaffold Hugo + Decap trilingual site from WordPress migration plan 2026-07-31 11:17:32 +00:00
README.md Add infra files and step-by-step server setup guide 2026-07-31 11:36:43 +00:00

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

  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. The Caddyfile and Docker Compose files it uses live in infra/.

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-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

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