Diario di bordo · Fase 0 · 7/7
La SEO multilingue di aubia.dev: URL, hreflang e JSON-LD
Le sei lingue di aubia.dev hanno URL e link hreflang propri. L'HTML servito collega anche gli articoli al loro autore, senza garantire l'indicizzazione né le citazioni da parte dell'IA.
- seo
- i18n
- geo
- inertia

Una visita a /fr/blog/multilingual-seo-geo deve restituire questo articolo in francese, anche se il browser preferisce l'inglese. Nemmeno il cookie di lingua deve modificare il contenuto di quell'URL. Per condividere una traduzione o farla indicizzare, ho bisogno che il suo indirizzo identifichi sempre la stessa lingua.
Le pagine pubbliche di aubia.dev usano sei prefissi: /fr, /en, /es, /de, /it e /pt. Gli slug restano in inglese in tutte le versioni. Il prefisso basta a distinguerle.
Il rendering lato server di Inertia fornisce il contenuto e i metadati nella risposta HTML iniziale. Un crawler può leggerli senza eseguire il JavaScript della pagina. Questo presuppone che l'SSR funzioni: in caso di errore, la configurazione del sito consente il ripiego sul rendering lato client. Il contenuto dipende allora da JavaScript, come spiega in dettaglio l'articolo dedicato a questa stack.
La lingua dell'URL e quella del browser
Il middleware SetLocale sceglie la lingua prima del rendering. Consulta il segmento dell'URL, poi il cookie di preferenza, poi l'header Accept-Language del browser e infine la configurazione dell'applicazione. Usa il primo valore riconosciuto.
Su una pagina sotto /fr, il segmento dell'URL ha quindi sempre la precedenza. Su / non c'è un segmento di lingua: il cookie, le preferenze del browser e la lingua predefinita, l'inglese, determinano la destinazione del reindirizzamento.
Per leggere Accept-Language, il server scorre le lingue in ordine di preferenza e confronta le loro prime due lettere con le sei lingue supportate. Se nessun cookie ha la precedenza, un browser che richiede pt-BR può quindi essere reindirizzato a /pt.
La radice risponde con un reindirizzamento 302. Un 301 indicherebbe uno spostamento permanente, mentre la destinazione dipende dal visitatore e dalla sua preferenza del momento.
Una risposta che varia in base alla richiesta
Una cache condivisa non deve riutilizzare un reindirizzamento al francese per un visitatore che richiede l'italiano. L'header Vary indica quali header della richiesta distinguono le risposte.
Inertia imposta Vary: X-Inertia per separare le risposte HTML da quelle JSON. Il suo middleware sostituisce questo header, quindi SetLocale aggiunge i valori relativi alla lingua dopo il ritorno da quel middleware, solo sulla route di reindirizzamento:
if ($request->routeIs('home.redirect')) {
$response->headers->set('Vary', ['Cookie', 'Accept-Language'], replace: false);
}
La risposta comprende così X-Inertia, Cookie e Accept-Language. Le pagine localizzate conservano il Vary di Inertia, senza aggiunte per la negoziazione della lingua: il loro URL la determina già.
Questo meccanismo dipende dalla cache che riceve la risposta. Cloudflare non tiene conto di tutti i valori di Vary per impostazione predefinita. Inviare l'header non basta quindi a dimostrare che la CDN distingua queste varianti; le sue regole di cache devono essere compatibili con le risposte servite.
Hreflang collega le traduzioni
Un URL distinto per ogni lingua rende ciascuna versione accessibile separatamente. L'annotazione hreflang indica ai motori di ricerca quali pagine sono traduzioni l'una dell'altra.
Ogni versione elenca il proprio URL e quelli delle altre lingue disponibili. Google richiede questi link reciproci: se due pagine non rimandano l'una all'altra, le annotazioni di quella coppia possono essere ignorate. Le coppie con rimandi reciproci possono comunque essere elaborate.
Uso codici di sola lingua: fr, en, es, de, it e pt. Il sito propone una versione portoghese destinata ai lusofoni, non contenuti distinti per il Portogallo e il Brasile. pt descrive questa scelta; pt-PT indicherebbe contenuti in portoghese destinati al Portogallo.
L'annotazione informa il motore di ricerca sul pubblico a cui ci si rivolge. Il reindirizzamento HTTP della radice sceglie invece la destinazione di una visita in base alla richiesta ricevuta.
Una versione di ripiego
L'annotazione x-default identifica una versione di ripiego per le lingue o le regioni non coperte. Su aubia.dev punta all'inglese. Per un articolo la cui traduzione inglese non è pubblicata, viene omessa: un URL che restituisce 404 non sarebbe una destinazione di ripiego utile.
Le annotazioni del blog si limitano alle traduzioni pubblicate dello stesso slug. Un articolo disponibile in tre lingue elenca quelle tre versioni, anche se il resto del sito ne supporta sei.
Il componente SeoHead scrive questi link nel <head>. Anche la sitemap li genera a partire dalle lingue pubblicate. I due metodi sono equivalenti per Google; usarli entrambi non migliora il posizionamento nei risultati di ricerca. Mantenerli entrambi significa soprattutto assicurarsi che le destinazioni coincidano.
Un URL canonico per ogni lingua
Il link canonical risponde a un'altra domanda: quale URL preferire tra pagine identiche o molto simili? Una traduzione completa non è un duplicato dell'originale solo perché tratta lo stesso argomento in un'altra lingua.
Ogni versione di un articolo dichiara quindi il proprio URL canonico. La pagina francese non indica quella inglese come canonica. Google raccomanda una destinazione nella stessa lingua, quando esiste, e conserva la decisione finale sull'URL da scegliere.
Le pagine di conferma e di cancellazione dalla lista d'attesa usano noindex,nofollow. Il sito non vi aggiunge link canonical né hreflang e le esclude dalla sitemap. Le annotazioni delle versioni linguistiche restano così riservate alle pagine destinate all'indicizzazione.
Open Graph usa un formato diverso: og:locale vale, per esempio, fr_FR. La specifica Open Graph definisce language_TERRITORY per questa proprietà facoltativa. Questo formato basato sul territorio non si applica alla selezione del pubblico tramite hreflang.
Link di lingua disponibili prima di JavaScript
Il selettore di lingua contiene veri link alle altre versioni della pagina. Google raccomanda questi link insieme alle annotazioni hreflang, affinché i visitatori possano scegliere autonomamente.
Il componente usa gli elementi HTML nativi details e summary. Le ancore vengono renderizzate anche quando il pannello è chiuso. Con l'SSR sono quindi presenti nell'HTML ricevuto, senza attendere un clic per essere aggiunte al documento.
Il browser gestisce l'apertura con il mouse o la tastiera. Un effetto React aggiunge la chiusura con Esc e con un clic all'esterno. Un menu che monta il contenuto solo all'apertura non fornirebbe questi link nel rendering iniziale; questo limite dipende dal comportamento del componente, non dalla sola presenza di un portal React.
Le destinazioni provengono dal server. Su un articolo sono limitate alle traduzioni pubblicate, come gli hreflang. Il selettore non propone una lingua in cui l'articolo manca.
Un autore identificato nel grafo JSON-LD
Il componente SeoHead descrive tre entità comuni in JSON-LD: l'organizzazione Aubia, me come persona e il sito. JSON-LD usa qui il vocabolario di schema.org per nominare tipi e relazioni.
Ogni entità ha un @id stabile. I riferimenti a questi identificatori dichiarano una relazione senza ripetere l'intero oggetto. Il grafo comprende i seguenti collegamenti, mostrati in un estratto limitato a tipi, identificatori e relazioni:
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://aubia.dev/#organization",
"founder": { "@id": "https://aubia.dev/#founder" }
},
{
"@type": "Person",
"@id": "https://aubia.dev/#founder",
"worksFor": { "@id": "https://aubia.dev/#organization" }
},
{
"@type": "WebSite",
"@id": "https://aubia.dev/#website",
"publisher": { "@id": "https://aubia.dev/#organization" }
}
]
}
Le pagine aggiungono i propri dati tramite additionalJsonLd. La home page fornisce un FAQPage con le domande e le risposte visibili. Un articolo aggiunge un BlogPosting e un BreadcrumbList, il suo breadcrumb.
Nel BlogPosting, author fa riferimento a #founder e publisher a #organization. L'autore dell'articolo viene quindi dichiarato come la stessa persona che ha fondato il sito. Aggiungere un oggetto a @graph non crea, da solo, una relazione con tutti gli altri oggetti: sono le proprietà e i loro riferimenti a descrivere quelle relazioni.
La firma visibile di ogni articolo rimanda allo stesso profilo pubblico del nodo Person. I metadati Open Graph dichiarano il tipo article e le date di pubblicazione e modifica.
Il grafo non dichiara né SoftwareApplication né un'offerta di preordine: il sito propone una lista d'attesa, non un download del software. Non vengono inventate recensioni o valutazioni per ottenere un risultato avanzato. Le FAQ visibili conservano il markup, identificato dall'URL della pagina localizzata, ma Google non mostra più risultati avanzati FAQ dal 7 maggio 2026. Queste dichiarazioni non garantiscono il loro utilizzo da parte di un motore né l'indicizzazione.
La GEO, senza garanzia di citazione
Il termine GEO, da Generative Engine Optimization, indica le pratiche volte a migliorare la visibilità dei contenuti nelle risposte generate dall'IA. Non è un protocollo comune che assicuri che una pagina venga letta, selezionata o citata.
Per apparire come link a una fonte in AI Overviews o AI Mode di Google Search, una pagina deve essere indicizzata da Google e idonea alla visualizzazione con uno snippet. Non è richiesto alcun markup schema.org speciale. Anche una pagina che soddisfa queste condizioni non ha alcuna garanzia di apparire come fonte.
Cerco di scrivere sezioni comprensibili senza dover rileggere tutto l'articolo. Questo aiuta i lettori a seguire una spiegazione e a citarne un passaggio, senza garantire che un'IA lo riproduca fedelmente.
Un file llms.txt da mantenere
Il sito serve anche un file llms.txt. Contiene una presentazione del prodotto in Markdown e link a pagine pubbliche, senza un'interfaccia da percorrere. Si ispira alla proposta llms.txt, distinta dalle regole di scansione di robots.txt.
Questo file viene scritto separatamente dal sito e dagli articoli. Le sue affermazioni e i suoi link vanno quindi verificati quando cambiano altrove. La sua presenza non dimostra che un motore lo legga né che migliori il posizionamento o le citazioni del sito.
La pubblicazione delle traduzioni
Un articolo del blog corrisponde a un file Markdown per ogni lingua. La data di pubblicazione viene letta dal front matter, in UTC. Il build compila il Markdown e l'evidenziazione Phiki in un catalogo locale obbligatorio in produzione, inclusi gli articoli futuri. Le date vengono verificate a ogni lettura: prima della data, l'URL restituisce 404; una volta raggiunta, l'articolo diventa accessibile senza un nuovo deploy. Un catalogo assente, non valido o obsoleto produce una risposta 503 sulle route che ne dipendono, senza eseguire Phiki durante la richiesta.
L'indice lo elenca e il feed RSS ne pubblica il riassunto con la data di pubblicazione. La sitemap usa la data di aggiornamento, oppure quella di pubblicazione se la prima manca, per lastmod. Aggiunge le annotazioni hreflang delle traduzioni disponibili. La pagina stessa fornisce il BlogPosting e il breadcrumb.
Il feed RSS e la sitemap sono serviti senza sessioni né cookie e indicano una durata della cache HTTP di un'ora. Il loro aggiornamento in una cache intermedia o in un lettore di feed può avvenire dopo la disponibilità dell'articolo nell'applicazione. Il lastmod di ogni indice del blog corrisponde alla data di modifica o pubblicazione più recente dei suoi articoli pubblicati. Le pagine statiche non dichiarano una data sconosciuta. Il <head> delle pagine del blog contiene anche un link per individuare il feed RSS localizzato.
I testi dell'interfaccia seguono un processo diverso
I testi dell'interfaccia sono memorizzati nei file lang/<locale>.json. Il francese è la lingua sorgente, con l'inglese come pivot per spagnolo, tedesco, italiano e portoghese.
Quando viene committata una modifica al file francese, l'hook invia all'agente di traduzione le chiavi aggiunte o modificate che non sono escluse. Unisce i valori ricevuti al file inglese e conserva gli altri. Le eliminazioni di chiavi vengono applicate separatamente. La propagazione dall'inglese alle altre quattro lingue si avvia con un comando manuale.
Tre prefissi sono esclusi dalle aggiunte e dalle modifiche automatiche: usp.*, features.tagline.* e letter.body.*. I primi due non hanno chiavi nei dizionari attuali. Il terzo protegge il corpo della Lettera del fondatore. Il claim e il titolo del banner della home page non rientrano in queste esclusioni, quindi possono essere tradotti automaticamente.
Il server carica le traduzioni della lingua richiesta e completa le chiavi mancanti con l'inglese. L'hook React useT() legge poi i valori condivisi nelle props Inertia. Gli articoli non passano da questo script: i loro file Markdown vengono tradotti separatamente.
Il corpo della Lettera esiste solo nei dizionari francese e inglese. In spagnolo, tedesco, italiano e portoghese, i suoi paragrafi appaiono quindi in inglese, anche se i titoli e le etichette circostanti sono tradotti. Il loro contenitore dichiara lang="en": questa annotazione ne identifica la lingua senza tradurli.
Questo cantiere si racconta qui, articolo dopo articolo. Ciò che viene dopo dipende da ciò che lei ne dirà.
Si iscriva alla lista d'attesa