Un blog d’una llengua, tres dies després de tres
Fa poc teníem una pàgina web en Astro amb una única llengua. La setmana passada, el mateix blog servia en castellà, català i anglès, amb un selector d’idioma, feeds de RSS i sitemap per llengua, i un desplegament que es feia sol des de Git.
No va ser màgia. Va ser una pyme agèntica: un conjunt d’agents de Hermes treballant amb GitOps (Flux CD), un model local (Qwen 3.8 27B) i una eina com OpenCode, sobre una base d’infraestructura que ja teníem muntada. El que vull dir-te no és l’exploit: és com ho vam fer, pas a pas, amb el detall suficient perquè el puguis replicar. I el que vam aprendre de cost pel camí.
Aquest post narra un cas real del repositori ia_develop/astro. El treball es va fer a la branca develop, però avui el blog viu a la branca main (tag 0.0.2) amb i18n complet, en producció. No és una ficció: cada xifra que apareix aquí surt de l’historial de git, de l’estat de Kanban o dels logs. Si alguna cosa no és real, no hi és.
La pyme agèntica: qui va fer què
La primera cosa que cal entendre és que no hi va haver “una persona treballant 60 hores”. Hi va haver diversos agents treballant en hores disperses, coordinats per un tauler Kanban que era la font de la veritat. Així es va repartir el treball de la migració i18n (18 commits, del 19 al 25 d’agost, a la branca develop, després fusionada a main amb el tag 0.0.2):
| Qui | Tipus de treball | Exemple real (commit) |
|---|---|---|
| Hermes (agent principal) | Framework i18n, localització de contingut, routing, feeds | feat(i18n): content layer per-locale (es/ca/en), feat(i18n): per-locale RSS + sitemap feeds |
| OpenCode | UI i millores amb suport directe d’operador amb livecoding | i18n: extract UI strings to locale files (nav, about, index) |
| Humans (operadors) | Revisió, decisions, contingut | feat(i18n): localize blog index per-locale, i18n(content): add ca + en versions… |
La clau no és que els agents escrivissin sols. És que el treball es va dividir en tasques petites i verificables, cadascuna amb un resultat comprovable. Kanban era el tauler; Git era la pista; la revisió humana, el control de qualitat.
Això és important: un agent no “resol” el problema sol. Un agent fa una tasca, la deixa en un estat verificable, i el següent la recull. La coordinació no està al cap d’un agent, està al tauler.

Evidència: el tauler de Hermes (Kanban) que va fer de font de la veritat durant la migració i18n.
Com ho vam fer, pas a pas
El flux va tenir cinc fases. Cadascuna és una tasca de Kanban amb el seu propi resultat i la seva pròpia revisió.
1. Investigar abans d’escriure
No es planifica sense dades. Abans de tocar res, l’agent es va preguntar: què hi ha ja, què hi ha al cluster, què ha fet cadascú? Es van inventariar:
- Els taulers de Kanban (quin treball hi havia:
astro-blog-i18n,astro-ai-blog-delivery, etc.). - El flux GitOps (com es construeix i desplega la imatge).
- La documentació (
docs/, ADRs, runbooks). - Els commits per autor (què va fer Hermes vs. OpenCode vs. l’humà).
Sense aquesta evidència, el post seria una invenció. Amb ella, és un relat veraz.

Evidència: tantes addicions per tan poc temps… és una bogeria.
2. Escriure el contingut (amb revisió)
El post s’escriu en la veu que llegim a diari, però signat per qui entén la tècnica. Aquí apliquem una regla innegociable: tota tasca que genera contingut, codi o manifests acaba amb una revisió. L’esborrany no es publicava sense passar per un ull humà.
3. Verificar el build
npm run build && npm run preview
# i comprovar que el post apareix a les tres llengües
grep -c '<slug>' dist/rss.xml dist/sitemap.xml # >= 1 per llengua
Un build verd i els feeds correctes a les tres llengües són el tancament d’aquesta fase.
4. Obrir el PR (mai a main directament)
Mai un commit directe a main. Es crea una feature branch i s’obre un pull request. Aquí el treball de l’agent acaba: l’humà decide si fa el merge.
5. Merge + desplegament (humà)
L’operador fa el merge, Flux reconcilia, el Image Update Automation consume la branca i desplega. L’agent no fa el merge ni desplega a producció. Aquesta decisió és humana, sempre.
La regla que ho sosté tot: tot el treball passa per Kanban, i tota tasca que genera codi, manifests o scripts acaba amb una revisió requerida.
Detall tècnic replicable
Aquí ve el que pots copiar. Tot el codi que segueix és real i està a la branca main (tag 0.0.2) de ia_develop/astro.
a) Configurar les llengües (src/config.ts)
Les llengües es defineixen una vegada, en un sol lloc:
export const locales = ['es', 'ca', 'en'] as const;
export const defaultLocale = 'es';
export type Locale = (typeof locales)[number];
// Noms propis (autònoms) de cada llengua, en un únic lloc
export const localeNames: Record<Locale, string> = {
es: 'Español',
ca: 'Català',
en: 'English',
};
La idea és que els labels de navegació no estiguin “dures” al codi, sinó que es resolguin per llengua. L’array navItems guarda key, i l’etiqueta es busca al catàleg de la llengua activa.
b) Rutes per llengua amb Astro (src/pages/[locale]/index.astro)
Astro fa routing per fitxers. Si poses un fitxer a la carpeta [locale]/, genera una variant per cada llengua configurada:
export const getStaticPaths = () =>
locales.map((locale) => ({ params: { locale }, props: { locale } }));
const { locale } = Astro.props as { locale: (typeof locales)[number] };
const t = getMessages(locale);
Un únic index.astro dins de [locale]/ produeix /es, /ca i /en sense duplicar el codi.
c) Resoldre la llengua i les dates (src/lib/i18n.ts)
import { getLocaleByPath } from 'astro:i18n';
import { locales, defaultLocale, type Locale } from '../config';
import es from '../locales/es.json';
import ca from '../locales/ca.json';
import en from '../locales/en.json';
const messages: Record<Locale, typeof es> = { es, ca, en };
export const getMessages = (locale: string | undefined) => {
const key =
(locale && (locales as readonly string[]).includes(locale)
? (locale as Locale)
: defaultLocale) as Locale;
return messages[key];
};
Els strings de la interfície (navegació, sobre, índex) s’extreuen a src/locales/*.json. El contingut dels posts viu a src/content/blog/{es,ca,en}/<slug>.md.
d) Contingut per llengua sense col·lisions (src/content.config.ts)
L’astuc que fa que tres llengües del mateix post no es pissin és el generateId:
const blog = defineCollection({
loader: glob({
pattern: '**/*.md',
base: './src/content/blog',
generateId: ({ entry, data }) => {
// entry = "ca/getting-started-with-astro.md"
const rel = entry.replace(/\\/g, '/').replace(/^\.?\/+/, '');
const [folder, file = ''] = rel.split('/');
const slug = data?.slug || file.replace(/\.md$/i, '') || rel;
// id únic: <locale>-<slug>
return LOCALES.includes(folder) ? `${folder}-${slug}` : slug;
},
}),
schema: z.object({
title: z.string(),
slug: z.string().optional(),
locale: z.enum(LOCALES as [string, ...string[]]).default(defaultLocale),
description: z.string(),
pubDate: z.date(),
tags: z.array(z.string()),
author: z.string().optional(),
image: z.string().optional(),
featured: z.boolean().optional(),
}),
});
Sense aquest generateId, dues localitzacions del mateix slug col·lisionarien al store i una enborraria l’altra. L’id com a <locale>-<slug> resol el conflicte.
e) El desplegament amb GitOps (el CI de la branca main)
El flux que converteix un commit en un desplegament és GitOps pur. El CI de Forgejo emet una tag cronològica immutable:
# .forgejo/workflows/build-image.yml (resum)
env:
REGISTRY: registry.cris-mora-cv.es
UNIXTS: $(date +%s) # tag immutable: 0.0.2
# develop -> :develop (preview), main -> :launch (producció, tag 0.0.2)
L’execució d’aquests jobs no es fa en una màquina fixa: Forgejo dispara la workflow (git actions) en pods efímers controlats per KEDA. KEDA escala els workers de Forgejo des de zero segons els esdeveniments de la cua, arrenca el pod només pel job i el destrueix en acabar; així no hi ha infraestructura de CI ociosa, només es consumeix el just per desplegament.

Evidència: el flux de build a Forgejo Actions (git actions) executant-se en pods efímers escalats per KEDA.
L’Image Update Automation de Flux consume aquella branca i una política numèrica:
ImagePolicy: pattern '^0.0.2-' + policy.numerical desc
-> el build més recent (el unixts més gran) guanya, i es desplega sol.
Un commit a main → Forgejo llança el job en un pod efímer (KEDA) → el CI emet la imatge 0.0.2-<unixts> → la IUA detecta la nova imatge → Flux reconcilia → el blog s’actualitza. Sense tocar el cluster a mà.
Les skills: com Hermes recorda allò que aprèn
Una part que sovint es passa per alt: Hermes no reinventa la roda en cada tasca. Fa servir skills, procediments que es guarden i es reutilitzen. Algunes de les que va aplicar aquest treball:
astro-blog— crear i publicar posts (capa de contingut, feeds, build, deploy).flux-gitops-integration— auditar GitRepos i Kustomizations de Flux.flux-image-automation— depurar quan la IUA no reconcilia.kanban-board-management/kanban-worker— el tauler com a font de la veritat.forgejo-git/forgejo-http-troubleshooting— operacions Git amb Forgejo intern.
El interessant és que algunes skills s’auto-aprenen: quan un flux és difícil i es resol, Hermes el guarda com a skill per no tornar-lo a enfrontar. És memòria procedural: no només recorda què va passar, sinó com es va fer.
Xifres i reflexió: el cost de la IA ja no és el que era
Anem al concret, perquè aquí és el que de veritat m’importava transmetre.
La migració i18n va deixar 18 commits a la branca develop (fusionades a main, tag 0.0.2), del 19 al 25 d’agost — una finestra d’uns 6 dies. Vam tenir 7 posts a les tres llengües i una infraestructura (rutes, feeds, switcher, routing) completa. El treball es va fer en hores disperses, repartit entre diversos agents i un parell d’hums.
Això no vol dir que no costés res. Vol dir que el cost va deixar de ser lineal amb les hores-home. Abans, passar un blog a tres llengües era una setmana d’una persona, i després una altra setmana per mantenir-lo. Ara és una activitat d’hores disperses que un parell d’agents coordinateixen, amb un humà al final revisant.
I aquí és la reflexió que vull deixar-te: imagina el que es podria fer amb hardware empresarial — un servidor amb GPUs de veritat — davant la màquina en què es va provar això, una DGX Spark amb GB10. Si amb una màquina d’escriptori executant un model local (Qwen 3.8 27B) vam poder fer això en una pyme agèntica, el salt de qualitat amb infraestructura seria no és incremental: és d’una altra ordre.
La conclusió que m’emporto és aquesta: el coll d’ampolla ja no és el hardware ni el preu del model. És saber preguntar bé i coordinar bé. Amb recursos locals, el cost d’una “pyme agèntica” s’apropa al no-res en comparació amb el que costava una persona sencersa dedicant-se a tasques repetides. El futur no és “més humans treballant més hores”; és “millor coordinació entre agents, amb humans als punts que importen”.
Clausura
Vam passar d’una llengua a tres. No va ser un miracle: va ser una pyme agèntica treballant amb Kanban com a font de la veritat, GitOps com a autopista, un model local com a cervell i una revisió humana al punt de decisió.
Si vols replicar-lo, el codi que has vist a dalt està a ia_develop/astro (branca main, tag 0.0.2). Si vols copiar el mètode, la regla és simple: divideix el treball en tasques petites, verifica’l i deixa la decisió important per a l’humà.
Aquest post tanca l’arc del blog amb allò aconseguit: la migració i18n en una setmana. Allò que ve a continuació és el roadmap de com vam muntar el nostre homelab com a pyme de desenvolupament — la infraestructura (cluster, GitOps, models locals, Forgejo i KEDA) que va fer possible tot el que has vist aquí.
I si et queda la pregunta de “i amb hardware de veritat?”, la resposta és la que hauria de deixar més clar aquest post: amb una màquina d’escriptori ja es pot; amb hardware empresarial, el que es pugui fer avui és difícil d’imaginar.