Add hand-rolled English support (/en/*) alongside the Italian site

PocketBase gains optional, manually-authored English fields on articles
and categories (same record, same slug), and the frontend serves an /en
counterpart of every dynamic route via thin page wrappers around shared
view components — no i18n library, consistent with the project's existing
minimalism stance for a two-locale site with ~20 UI strings.

An article/category with no translation 404s cleanly on its own /en
detail page and is filtered out of /en listings, and the header's
language-switch link falls back to the English blog index rather than a
dead link; resolving that requires a global route middleware, since the
layout's header renders before the page content in document order and so
can't react to state a page component sets during its own async setup.
This commit is contained in:
2026-09-11 13:53:17 +02:00
parent 847edc7397
commit 33b49c5dbb
40 changed files with 1285 additions and 576 deletions
+14 -7
View File
@@ -46,10 +46,17 @@ dominio, un solo certificato TLS. Se cambi questa scelta di routing, spiega il t
(`/api/files/...`, sola lettura, nessun'autenticazione richiesta né concessa).
- Il Markdown dell'articolo è convertito in HTML **nell'endpoint**, non nel componente: il
contenuto è già nell'HTML SSR e `marked` resta fuori dal bundle client.
- L'**interfaccia** è solo in italiano, stringhe statiche nei componenti (niente
`@nuxtjs/i18n`, niente file di traduzione): `<html lang="it">` è fisso in
`nuxt.config.ts`. La traduzione per i visitatori stranieri è delegata all'estensione
Google Translate del browser, non è gestita dall'app. I **contenuti** restano monolingua.
- Il sito è **bilingue** (italiano, sorgente, e inglese), fatto in casa senza `@nuxtjs/i18n`
file di traduzione runtime: due lingue, ~20 stringhe statiche, nessun plurale — la libreria
costerebbe più codice di quanto risolva (il fallback su contenuti non tradotti, la
localizzazione degli endpoint Nitro e gran parte della SEO restano comunque da scrivere a mano).
`<html lang>` è dinamico (per pagina, via `useSeo`), non più fisso. Le pagine inglesi vivono sotto
prefisso `/en/*`, come wrapper sottili attorno agli stessi componenti "view" delle pagine
italiane — vedi `docs/frontend.md#english-content`. Le stringhe UI statiche vivono **solo** in
`frontend/shared/utils/i18n.ts`. Gli **articoli** hanno campi paralleli opzionali
(`titleEn`/`contentEn`/`coverAltEn`, `nameEn` su categorie), tradotti **a mano** dall'editor in
admin — nessuna traduzione automatica. Vedi `docs/content-model.md#english-content` per lo
schema e la politica di fallback (404 pulito sul dettaglio non tradotto, filtro nelle liste).
- `pocketbase/pb_migrations/*.js` definisce collection e regole: `listRule`/`viewRule` pubblici
solo su `articles` (solo pubblicati, via `publishedAt`) e `categories`; `createRule`/
`updateRule`/`deleteRule` sempre `null` (solo superuser). Qualsiasi permesso in più va motivato.
@@ -107,8 +114,8 @@ richiede `sudo`; se mancano librerie di sistema per il browser, installarle a pa
| Type | Campi |
|---|---|
| Article | title, slug, content (Markdown), cover, coverAlt, category (rel), publishedAt, authorName |
| Category | name, slug |
| Article | title, slug, content (Markdown), cover, coverAlt, category (rel), publishedAt, authorName, titleEn, contentEn, coverAltEn |
| Category | name, slug, nameEn |
Deliberatamente minimale: niente tag, nessun campo SEO separato. La meta description è ricavata
dall'inizio del body (`summarise` in `frontend/server/utils/pocketbase.ts`), la data è
@@ -120,7 +127,7 @@ codice applicativo. Le URL pubbliche usano lo **slug**, mai l'id del record.
## Frontend
Rotte: `/`, `/blog`, `/blog/[slug]`, `/category/[slug]`.
Rotte: `/`, `/blog`, `/blog/[slug]`, `/category/[slug]`, ciascuna con equivalente `/en/...`.
- SSR o prerender per tutto ciò che è indicizzabile. Mai pagine blog client-only senza motivo scritto.
- `<script setup lang="ts">`, Composition API. Convenzioni Nuxt standard (`pages/`, `components/`,