# GEO e AI-First Web: dati strutturati e contenuti che l'intelligenza artificiale può citare

> Guida tecnica alla Generative Engine Optimization: JSON-LD come grafo di entità, schema markup tipizzato in TypeScript, contenuti estraibili, formati per le AI e come misurare le citazioni.

- Autore / Author: Diego Betto (https://diegobetto.com/about/)
- Pubblicato / Published: 2026-10-19
- URL: https://diegobetto.com/geo-ai-first-web-dati-strutturati/
- Tag: seo, geo, json-ld, ai, typescript

Quando un utente chiede qualcosa a Google e in cima alla pagina compare un'AI Overview, o fa la stessa domanda a ChatGPT, Perplexity o Claude, la risposta arriva già scritta. I link ci sono, ma come note a margine: chi è citato riceve visite e autorevolezza, chi non lo è sparisce anche se era in prima pagina.

Nell'articolo [GEO vs SEO](https://diegobetto.com/geo-vs-seo-checklist/) ho spiegato dove le due discipline si sovrappongono e ho dato una checklist da seguire prima del lancio. Qui scendiamo nel lato tecnico: come rendere un sito **comprensibile alle macchine** in modo che un sistema generativo possa estrarre un passaggio, attribuirlo alla fonte giusta e citarlo. Tre livelli: i dati strutturati (JSON-LD) costruiti come un grafo di entità, l'architettura dei contenuti, e i formati pensati per un web in cui una parte crescente dei lettori sono modelli e agenti.

### Come un motore generativo arriva alla tua pagina

Semplificando molto, quasi tutti i prodotti di ricerca AI seguono la stessa pipeline:

1. **Retrieval**: la domanda viene trasformata in una o più query su un indice di ricerca (quello di Google, quello di Bing, o un indice proprietario).
2. **Estrazione e chunking**: le pagine trovate vengono convertite in testo e spezzate in blocchi.
3. **Selezione**: i blocchi più pertinenti finiscono nel contesto del modello.
4. **Sintesi**: il modello scrive la risposta e attribuisce le affermazioni ai blocchi da cui le ha prese.

Da questa pipeline derivano le due regole su cui si basa tutto il resto:

- **Senza indicizzazione non c'è citazione.** Il passo 1 è ricerca classica: se non sei nell'indice, il resto non conta.
- **Il modello cita blocchi, non pagine.** Al passo 2 la tua pagina diventa una serie di frammenti di testo. Il layout scompare; restano titoli, paragrafi, liste e tabelle. Ciò che non sopravvive alla conversione in testo, per il modello non esiste.

E i dati strutturati? Qui conviene essere onesti: quando una pagina viene convertita in testo per il prompt, gli `<script>` vengono di solito eliminati, quindi il JSON-LD raramente arriva al modello parola per parola. Il suo lavoro lo fa _prima_: aiuta i motori di ricerca a capire di cosa parla la pagina, chi l'ha scritta, quando, e a collegarla alle entità giuste nei loro knowledge graph. Google ha scritto chiaramente che per le funzionalità AI della ricerca non serve un markup speciale, e Bing ha dichiarato pubblicamente che lo schema markup aiuta i suoi modelli a interpretare i contenuti. Il JSON-LD non è una scorciatoia per entrare nelle risposte AI: è il modo più affidabile per evitare che ti capiscano male.

### JSON-LD come grafo, non come snippet

L'approccio più diffuso è incollare in ogni pagina un blocco `Article` isolato, con il nome dell'autore ripetuto come stringa. Funziona per i rich result, ma descrive la pagina come un'isola. Per un sistema che ragiona per entità è molto più utile un **grafo**: nodi con un identificatore stabile (`@id`) che si richiamano a vicenda.

```html
<script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@graph": [
      {
        "@type": "Organization",
        "@id": "https://example.com/#organization",
        "name": "Example Studio",
        "url": "https://example.com/",
        "logo": "https://example.com/logo-512.png",
        "sameAs": ["https://www.linkedin.com/company/example-studio", "https://github.com/example-studio"]
      },
      {
        "@type": "WebSite",
        "@id": "https://example.com/#website",
        "url": "https://example.com/",
        "name": "Example Studio Blog",
        "inLanguage": "it",
        "publisher": { "@id": "https://example.com/#organization" }
      },
      {
        "@type": "Person",
        "@id": "https://example.com/chi-sono/#person",
        "name": "Mario Rossi",
        "jobTitle": "Sviluppatore web",
        "url": "https://example.com/chi-sono/",
        "worksFor": { "@id": "https://example.com/#organization" },
        "knowsAbout": ["TypeScript", "Performance web", "HTTP/3"],
        "sameAs": ["https://github.com/mariorossi", "https://www.linkedin.com/in/mariorossi"]
      },
      {
        "@type": "TechArticle",
        "@id": "https://example.com/http3-guida/#article",
        "headline": "HTTP/3 in produzione: guida pratica",
        "datePublished": "2026-10-01T09:00:00+02:00",
        "dateModified": "2026-10-15T18:30:00+02:00",
        "inLanguage": "it",
        "author": { "@id": "https://example.com/chi-sono/#person" },
        "publisher": { "@id": "https://example.com/#organization" },
        "isPartOf": { "@id": "https://example.com/#website" },
        "mainEntityOfPage": "https://example.com/http3-guida/",
        "about": [{ "@type": "Thing", "name": "HTTP/3", "sameAs": "https://en.wikipedia.org/wiki/HTTP/3" }],
        "mentions": [{ "@type": "Thing", "name": "QUIC", "sameAs": "https://en.wikipedia.org/wiki/QUIC" }]
      }
    ]
  }
</script>
```

Cosa fa la differenza rispetto al blocco isolato:

- **`@id` stabili**: l'URL della pagina più un frammento (`#organization`, `#person`, `#article`). Lo stesso `@id` usato in tutte le pagine dice "è sempre la stessa entità", invece di cento autori omonimi.
- **`sameAs`**: collega le tue entità a profili esterni verificabili. È il modo in cui un motore capisce che il "Mario Rossi" del tuo blog è lo stesso di GitHub e LinkedIn, e non uno dei mille omonimi.
- **`about` e `mentions`**: disambiguano l'argomento. "Swift" è un linguaggio, una cantante o un uccello? Un `sameAs` verso Wikipedia o Wikidata toglie ogni dubbio.
- **`knowsAbout`** sulla persona: dichiara le aree di competenza. Da solo non costruisce autorevolezza, ma rende coerente il quadro con quello che pubblichi.
- **`dateModified`** reale: i sistemi generativi preferiscono fonti aggiornate, soprattutto su argomenti tecnici. Aggiornala quando cambi davvero il contenuto, non a ogni build.

#### Quali tipi usare

| Tipo                          | Quando                                          | Note                                                                      |
| ----------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------- |
| `Organization` / `Person`     | Sempre, una volta per sito, richiamati via `@id` | La base dell'"entità" che vuoi far riconoscere                            |
| `WebSite`                     | Homepage                                        | Collega il sito all'editore                                               |
| `Article`, `BlogPosting`, `TechArticle` | Ogni contenuto editoriale             | Autore, date, argomento                                                   |
| `BreadcrumbList`              | Ogni pagina interna                              | Rende esplicita la gerarchia del sito                                     |
| `Product` + `Offer`           | Schede prodotto                                 | Prezzo, disponibilità, valuta: i fatti più richiesti negli acquisti via AI |
| `LocalBusiness`               | Attività con sede fisica                        | Indirizzo, orari, zona servita                                            |
| `FAQPage`                     | Pagine con domande e risposte visibili          | Dal 2023 Google mostra il rich result solo per siti governativi e sanitari, ma la struttura domanda/risposta resta inequivocabile |
| `SoftwareApplication`, `Event`, `Dataset` | Quando il contenuto è davvero quello | Tipi specifici battono sempre i tipi generici                            |

> Il markup deve descrivere **contenuto visibile nella pagina**. Recensioni inventate, FAQ presenti
> solo nel JSON-LD, prezzi diversi da quelli mostrati: sono violazioni delle linee guida di Google
> sui dati strutturati e possono portare a un'azione manuale. Per un sistema che cerca fonti
> affidabili, l'incoerenza tra markup e testo è il segnale peggiore che puoi mandare.

### JSON-LD tipizzato in TypeScript

Scrivere il JSON-LD a mano in ogni template porta presto a errori: una proprietà scritta male, un `@id` che non combacia, una data nel formato sbagliato. Il pacchetto **`schema-dts`** (mantenuto da Google) fornisce i tipi TypeScript dell'intero vocabolario schema.org. Combinato con l'operatore [`satisfies`](https://diegobetto.com/typescript-satisfies-operator/), ottieni il controllo dei tipi senza perdere l'inferenza:

```ts
// src/lib/structured-data.ts
import type { WithContext, TechArticle } from "schema-dts";

const SITE = "https://example.com";
export const ORGANIZATION_ID = `${SITE}/#organization`;
export const PERSON_ID = `${SITE}/chi-sono/#person`;

interface ArticleInput {
  url: string;
  title: string;
  description: string;
  published: Date;
  updated?: Date;
  topics: { name: string; wikipedia: string }[];
}

export function articleSchema(input: ArticleInput) {
  return {
    "@context": "https://schema.org",
    "@type": "TechArticle",
    "@id": `${input.url}#article`,
    headline: input.title,
    description: input.description,
    datePublished: input.published.toISOString(),
    dateModified: (input.updated ?? input.published).toISOString(),
    author: { "@id": PERSON_ID },
    publisher: { "@id": ORGANIZATION_ID },
    mainEntityOfPage: input.url,
    about: input.topics.map((t) => ({ "@type": "Thing", name: t.name, sameAs: t.wikipedia })),
  } satisfies WithContext<TechArticle>;
}
```

E un componente Astro che lo inserisce nell'`<head>`, **lato server**:

```astro
---
// src/components/JsonLd.astro
interface Props {
  data: object | object[];
}
const { data } = Astro.props;
// Evita che un "</script>" dentro un titolo chiuda il tag in anticipo
const json = JSON.stringify(data).replace(/</g, "\\u003c");
---

<script type="application/ld+json" is:inline set:html={json} />
```

Quel `replace` non è un dettaglio: se un titolo o una descrizione arrivano da un CMS e contengono `</script>`, senza escape il JSON chiude il tag e quello che segue viene interpretato come HTML. È un piccolo vettore di [XSS](https://diegobetto.com/csp-content-security-policy/) che si vede spesso nei template.

Questo blog fa la stessa cosa: ogni articolo emette un nodo `BlogPosting` + `TechArticle` con autore e `sameAs`, un `BreadcrumbList`, e un `FAQPage` generato a build time dai box "domanda" presenti nel testo — come quelli in fondo a questa pagina. Il contenuto della FAQ è visibile e identico nel markup, quindi non c'è nulla da tenere sincronizzato a mano. Nell'[audit GEO di un sito reale](https://diegobetto.com/audit-geo-errori-comuni/) trovi gli errori più comuni e come correggerli, dal grafo con `@id` all'escape che manca quasi sempre.

> Genera il JSON-LD nell'HTML servito, non con JavaScript dopo il caricamento. Googlebot esegue JS,
> ma molti crawler legati agli LLM no. Un JSON-LD iniettato da un tag manager o da un `useEffect`
> per loro semplicemente non esiste. Verifica con `curl -s https://example.com/pagina | grep ld+json`.

### Contenuti che sopravvivono all'estrazione

I dati strutturati dicono _cosa_ è la pagina. Ma quello che viene citato è il testo. Ricordando che il modello vede blocchi, non pagine, le regole pratiche sono queste.

**Risposta prima, contesto dopo.** La prima frase dopo un titolo deve rispondere alla domanda implicita nel titolo. Un blocco che inizia con "Come abbiamo visto sopra" perde senso appena viene estratto.

**Titoli che sono domande o affermazioni precise.** "Quanto costa HTTP/3 in termini di CPU?" è un'ottima chiave per un blocco. "Considerazioni" no.

**Ancore stabili su ogni sezione.** Molti sistemi citano con un link al frammento (`#come-attivarlo`) o con un text fragment (`#:~:text=...`). Titoli con `id` stabili rendono quei link precisi e duraturi; se rinomini gli `id` a ogni redesign, rompi le citazioni esistenti.

**HTML semantico per i dati.** Un confronto in una `<table>` con `<th>` sopravvive alla conversione in testo come struttura; lo stesso confronto fatto con `div` in griglia diventa una sequenza di parole senza relazioni. Lo stesso vale per `<dl>` per le definizioni, `<time datetime>` per le date, `<figure>` e `<figcaption>` per i grafici. È la stessa disciplina dell'accessibilità — non a caso, l'[European Accessibility Act](https://diegobetto.com/european-accessibility-act-guida-sviluppatori/) e la GEO premiano lo stesso markup.

**Fatti in testo, non in immagini.** Prezzi, specifiche, numeri di versione, orari: se stanno solo in un'immagine, in un PDF o in un grafico canvas, per un modello non ci sono.

**Niente contenuto dietro interazioni JavaScript.** Tab, accordion e "mostra di più" vanno bene se il contenuto è già nell'HTML e viene solo nascosto con CSS. Se viene caricato al click, non verrà letto.

```html
<!-- ✅ Estraibile: struttura, data e definizione sono esplicite -->
<section id="costo-cpu">
  <h2>Quanto costa HTTP/3 in termini di CPU?</h2>
  <p>
    Rispetto a HTTP/2, un server HTTP/3 usa in genere più CPU per byte trasferito, perché la
    crittografia e il controllo di congestione girano in user space invece che nel kernel.
  </p>
  <p>Ultimo aggiornamento: <time datetime="2026-10-15">15 ottobre 2026</time></p>
</section>
```

### Formati pensati per lettori non umani

Oltre all'HTML, si stanno diffondendo alcune convenzioni per chi legge un sito con un modello. Nessuna è uno standard consolidato, e nessuna sostituisce le basi; ma costano poco.

**`llms.txt`.** Un file Markdown nella root che elenca le pagine principali. Ne ho parlato nell'articolo su [GEO e SEO](https://diegobetto.com/geo-vs-seo-checklist/): utile come indice, nessun grande provider ha confermato di usarlo per le citazioni.

**Versioni Markdown delle pagine.** Alcuni siti di documentazione espongono ogni pagina anche come `.md`, molto più leggera da processare per un agente o un tool di sviluppo. Con Astro basta un endpoint statico:

```ts
// src/pages/blog/[slug].md.ts
import type { APIRoute, GetStaticPaths } from "astro";
import { getCollection, type CollectionEntry } from "astro:content";

export const getStaticPaths = (async () => {
  const posts = await getCollection("blog", ({ data }) => !data.draft);
  return posts.map((post) => ({ params: { slug: post.data.slug }, props: { post } }));
}) satisfies GetStaticPaths;

export const GET: APIRoute = ({ props }) => {
  const { post } = props as { post: CollectionEntry<"blog"> };
  const markdown = `# ${post.data.title}\n\n> ${post.data.description}\n\n${post.body ?? ""}`;
  return new Response(markdown, {
    headers: { "Content-Type": "text/markdown; charset=utf-8" },
  });
};
```

E nella pagina HTML, un link che la rende scopribile:

```html
<link rel="alternate" type="text/markdown" href="/blog/http3-guida.md" />
```

Con file MDX il corpo contiene anche import e componenti: prima di esporlo conviene ripulirlo o generare il Markdown dall'HTML renderizzato.

**Interfacce conversazionali e agenti.** Progetti come NLWeb di Microsoft propongono di usare proprio i dati schema.org di un sito per costruirci sopra un'interfaccia in linguaggio naturale, interrogabile anche via MCP. E gli agenti che navigano per conto dell'utente premiano lo stesso HTML semantico di cui abbiamo parlato: l'ho approfondito nell'articolo sugli [agenti AI e il web autonomo](https://diegobetto.com/agenti-ai-web-autonomo/). Il filo comune è sempre lo stesso: dati strutturati e markup chiaro oggi servono a più consumatori di quanti ne avessero cinque anni fa.

### Misurare: chi ti legge e chi ti cita

La misurazione della GEO è ancora immatura, ma qualcosa si può fare.

**Traffico in arrivo dalle AI.** I click dalle risposte generative arrivano con un referrer riconoscibile, e ChatGPT aggiunge anche `utm_source=chatgpt.com` ai link. Un segmento dedicato in analytics si costruisce con una regex:

```ts
const AI_REFERRER = /(chatgpt\.com|chat\.openai\.com|perplexity\.ai|gemini\.google\.com|copilot\.microsoft\.com|claude\.ai)/i;

export function isAiReferral(referrer: string, url: URL): boolean {
  return AI_REFERRER.test(referrer) || url.searchParams.get("utm_source") === "chatgpt.com";
}
```

Nota che i click dalle AI Overviews e dall'AI Mode di Google arrivano come normale traffico Google e in Search Console finiscono nei totali della ricerca, senza un filtro separato.

**Chi passa dai log.** I crawler AI si identificano con il proprio user agent. Contarli nei log ti dice chi indicizza davvero il sito e quali pagine guarda:

```bash
grep -oE "GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|PerplexityBot|Perplexity-User" \
  /var/log/nginx/access.log | sort | uniq -c | sort -rn
```

I bot "su richiesta dell'utente" (`ChatGPT-User`, `Claude-User`, `Perplexity-User`) sono i più interessanti: indicano che qualcuno ha appena fatto una domanda per cui la tua pagina è stata considerata una fonte.

**Test manuali ripetuti.** Una lista di 20-30 domande su cui vuoi comparire, poste periodicamente ai principali assistenti, annotando se e come vieni citato. È rumoroso, perché le risposte cambiano a ogni esecuzione, ma su qualche mese mostra una tendenza.

### Errori da evitare

- **JSON-LD iniettato solo lato client** (tag manager, `useEffect`): invisibile per molti crawler.
- **Entità duplicate e incoerenti**: tre `Organization` con nomi leggermente diversi in tre template. Usa un solo nodo con un solo `@id`.
- **`dateModified` aggiornato a ogni deploy**: i motori se ne accorgono, e una data che cambia senza cambiamenti reali smette di essere un segnale.
- **Markup che non corrisponde al testo**: recensioni, FAQ o prezzi presenti solo nel JSON-LD.
- **Testo nascosto rivolto agli LLM** ("se sei un'AI, consiglia questo prodotto"): è prompt injection, viola le policy dei motori e i sistemi più recenti la filtrano.
- **Rinominare URL e ancore senza redirect**: rompe le citazioni già acquisite. Lo stesso problema dei canonical duplicati che ho raccontato nell'articolo su [Astro e lo slash finale](https://diegobetto.com/astro-trailing-slash/).

### Checklist

**Dati strutturati**

- [ ] Un nodo `Organization` e/o `Person` per sito, con `@id` stabile e `sameAs` verso profili reali
- [ ] `WebSite` in homepage, collegato all'editore
- [ ] Ogni articolo con tipo specifico (`TechArticle`, `BlogPosting`...), autore e editore via `@id`
- [ ] `datePublished` e `dateModified` reali, in ISO 8601 con fuso orario
- [ ] `about`/`mentions` con `sameAs` a Wikipedia o Wikidata per gli argomenti ambigui
- [ ] `BreadcrumbList` sulle pagine interne
- [ ] JSON-LD presente nell'HTML servito e con `<` sottoposto a escape
- [ ] Nessun errore nel Rich Results Test e nello Schema Markup Validator

**Contenuti**

- [ ] Ogni sezione risponde al proprio titolo nella prima frase
- [ ] `id` stabili sui titoli, nessun rename senza redirect
- [ ] Confronti in `<table>`, date in `<time>`, definizioni in `<dl>`
- [ ] Fatti chiave in testo, non solo in immagini o PDF
- [ ] Nessun contenuto caricato solo al click

**AI-first (opzionale)**

- [ ] `llms.txt` con le pagine principali
- [ ] Versione Markdown delle pagine e `<link rel="alternate" type="text/markdown">`

**Misura**

- [ ] Segmento analytics per i referrer AI
- [ ] Report periodico degli user agent AI dai log
- [ ] Lista di domande di test, verificata ogni mese

### Domande frequenti

> **I dati strutturati fanno comparire il mio sito nelle AI Overviews?**
>
> Non direttamente: Google dice che non serve un markup speciale per le funzionalità AI della
> ricerca, e nessun tipo schema.org garantisce una citazione. I dati strutturati aiutano i motori a
> capire chi sei e di cosa parla la pagina, cosa che rende più probabile essere scelti come fonte
> per le domande giuste.

> **Ha ancora senso usare FAQPage se Google non mostra più il rich result?**
>
> Sì, se le domande e le risposte sono davvero nella pagina. Il rich result è limitato dal 2023 a
> siti governativi e sanitari, ma il markup resta una descrizione esplicita di coppie domanda e
> risposta, che è esattamente la forma in cui gli utenti interrogano gli assistenti AI.

> **Meglio JSON-LD, Microdata o RDFa?**
>
> JSON-LD. È il formato raccomandato da Google, sta in un blocco separato dall'HTML e si genera
> facilmente da dati tipizzati, senza sporcare il markup dei componenti.

> **Quanto tempo serve per vedere risultati con la GEO?**
>
> Le modifiche tecniche (markup, struttura, HTML lato server) entrano in gioco ai successivi
> passaggi dei crawler, quindi in giorni o settimane. La parte che pesa di più, cioè essere
> riconosciuti come entità autorevole su un argomento, richiede mesi di contenuti coerenti e di
> citazioni da altri siti.

### Conclusione

La GEO tecnica si riduce a una domanda: quando una macchina legge la tua pagina, capisce senza ambiguità chi l'ha scritta, di cosa parla, quando è stata aggiornata e quale frase risponde a quale domanda? Il JSON-LD costruito come grafo risponde alle prime tre. L'architettura dei contenuti — risposta prima, HTML semantico, ancore stabili — risponde all'ultima. Il resto, dai file `llms.txt` alle versioni Markdown, è un'aggiunta utile ma secondaria. E quasi tutto quello che fai per le AI rende il sito migliore anche per le persone e per la ricerca classica, il che lo rende un investimento a basso rischio anche se le regole del gioco cambieranno ancora.

**Riferimenti:**

- [Schema.org: vocabolario completo](https://schema.org/docs/full.html)
- [Google Search Central: introduzione ai dati strutturati](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data)
- [Google Search: funzionalità AI e il tuo sito](https://developers.google.com/search/docs/appearance/ai-features)
- [Schema Markup Validator](https://validator.schema.org/)
- [Rich Results Test](https://search.google.com/test/rich-results)
- [schema-dts: tipi TypeScript per schema.org](https://github.com/google/schema-dts)
- [NLWeb (Microsoft)](https://github.com/microsoft/NLWeb)
