Ottimizzazione SEO
Cosa otterrai: una sitemap, robots.txt, URL canonical, dati strutturati JSON-LD e una checklist per Google Search Console. Più come evitare che Cloudflare blocchi Googlebot.
La tua app deve avere NEXT_PUBLIC_SITE_URL impostata in Coolify (es. https://your-domain.com).
Sitemap e robots.txt
Next.js le genera in automatico da app/sitemap.ts e app/robots.ts.
sitemap.ts
Includi solo gli URL finali, non i redirect. Per i capitoli di writing che redirigono alla prima pagina, elenca gli URL della pagina reale (es. /writing/01-chapter/00-introduction), non lo slug del capitolo da solo. Usa lastModified dal contenuto quando disponibile.
import type { MetadataRoute } from "next";
import { getBlogPosts, getWritingPosts } from "@/lib/content";
const baseUrl =
process.env.NEXT_PUBLIC_SITE_URL || "https://your-domain.com";
export default function sitemap(): MetadataRoute.Sitemap {
const posts = getBlogPosts();
const writingPosts = getWritingPosts();
const postUrls: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${baseUrl}/engineering/${post.slug}`,
lastModified: post.meta.updated
? new Date(post.meta.updated)
: post.meta.date
? new Date(post.meta.date)
: new Date(),
changeFrequency: "monthly" as const,
priority: 0.8,
}));
const writingUrls: MetadataRoute.Sitemap = writingPosts
.filter((post) => post.slug.includes("/"))
.map((post) => ({
url: `${baseUrl}/writing/${post.slug}`,
lastModified: post.updated
? new Date(post.updated)
: post.date
? new Date(post.date)
: new Date(),
changeFrequency: "weekly" as const,
priority: 0.7,
}));
return [
{ url: baseUrl, lastModified: new Date(), changeFrequency: "weekly" as const, priority: 1 },
{ url: `${baseUrl}/engineering`, lastModified: new Date(), changeFrequency: "weekly" as const, priority: 0.9 },
{ url: `${baseUrl}/writing`, lastModified: new Date(), changeFrequency: "weekly" as const, priority: 0.9 },
...postUrls,
...writingUrls,
];
}
robots.ts
import type { MetadataRoute } from "next";
const baseUrl =
process.env.NEXT_PUBLIC_SITE_URL || "https://your-domain.com";
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: "*", allow: "/" },
sitemap: `${baseUrl}/sitemap.xml`,
host: baseUrl,
};
}
Check: dopo il deploy, apri https://your-domain.com/sitemap.xml e https://your-domain.com/robots.txt. Devono caricarsi entrambi.
URL canonical e meta
Gli URL canonical dicono ai motori di ricerca qual è l'URL "principale" di una pagina, evitando i problemi di duplicate content.
Metadata di layout (app/layout.tsx)
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || "https://your-domain.com"),
title: { default: "Your Site", template: "%s | Your Site" },
description: "Your description.",
openGraph: { type: "website", locale: "en_US", siteName: "Your Site" },
twitter: { card: "summary_large_image" },
robots: { index: true, follow: true },
};
Metadata per-pagina (es. blog post)
return {
title: post.meta.title,
description: post.meta.description,
alternates: { canonical: `${baseUrl}/engineering/${post.slug}` },
openGraph: {
title: post.meta.title,
description: post.meta.description,
type: "article",
publishedTime: post.meta.date,
modifiedTime: post.meta.updated,
url: `${baseUrl}/engineering/${post.slug}`,
},
twitter: { card: "summary_large_image", title: post.meta.title, description: post.meta.description },
};
Dati strutturati JSON-LD
I dati strutturati aiutano Google a capire i tuoi contenuti e possono abilitare i rich result.
Schema WebSite (homepage)
const jsonLd = {
"@context": "https://schema.org",
"@type": "WebSite",
name: "Your Site",
description: "Your description.",
url: baseUrl,
publisher: { "@type": "Person", name: "Your Name" },
};
// In JSX:
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
Schema Article (blog post e pagine writing)
Usa la stessa struttura per i post engineering (/engineering/slug) e le pagine writing (/writing/chapter/page). Includi authors, keywords (dai tag), e openGraph.publishedTime / modifiedTime nei metadata.
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.meta.title,
description: post.meta.description ?? undefined,
datePublished: post.meta.date ?? undefined,
dateModified: post.meta.updated ?? post.meta.date ?? undefined,
author: { "@type": "Person", name: "Your Name" },
publisher: { "@type": "Organization", name: "Your Site" },
url: canonicalUrl,
mainEntityOfPage: { "@type": "WebPage", "@id": canonicalUrl },
};
Validare: validator.schema.org, incolla l'URL di una pagina per controllarla.
Google Search Console
Step 1: aggiungi la property
- Vai su search.google.com/search-console
- Add property → scegli Domain (non URL prefix)
- Inserisci
your-domain.com - Verifica via DNS: aggiungi il record TXT che Cloudflare mostra al DNS del tuo dominio (Cloudflare → DNS → Add record → TXT)
Step 2: invia la sitemap
- Indexing → Sitemaps
- Inserisci
https://your-domain.com/sitemap.xml - Click su Submit
Step 3: controlla lo stato
- Status: Success: Google ha preso la sitemap. "Discovered pages" mostra quanti URL ha trovato.
- Status: Couldn't fetch: Google non riesce a raggiungere la sitemap. Vedi la sezione Cloudflare qui sotto.
- Temporary processing error: spesso si risolve in 24-48 ore. Prova Resubmit dal menu della sitemap.
URL Inspection: usa la barra di ricerca per ispezionare un URL. "Test live URL" controlla se Google riesce a prenderlo. "Request indexing" chiede a Google di crawlarlo.
Cloudflare e Googlebot
Se Search Console mostra "Couldn't fetch" sulla sitemap ma l'URL funziona dal browser, Cloudflare potrebbe star bloccando o sfidando Googlebot.
Security → Bots → Settings
- Bot Fight Mode: se è ON, può sfidare i bot. Assicurati che i bot verificati (incluso Googlebot) siano permessi, oppure disabilitalo temporaneamente per testare.
- Block AI bots: edita la regola e assicurati che Googlebot non sia nella lista di blocco. I crawler di training AI (GPTBot, ecc.) sono diversi da Googlebot.
- Managed robots.txt (AI Crawl Control): se Cloudflare gestisce il tuo robots.txt, può sovrascrivere quello dell'app. Controlla che il robots.txt servito includa
Sitemap: https://your-domain.com/sitemap.xml. Se no, aggiungilo in Cloudflare oppure disabilita Managed robots.txt così la tua app Next.js serve il proprio.
Errore Content-Signal: il Managed robots.txt di Cloudflare aggiunge una riga non standard Content-Signal: search=yes, ai-train=no. Google e PageSpeed Insights la segnalano come "Unknown directive" e riportano "robots.txt non è valido". Fix: disabilita Managed robots.txt in AI Crawl Control → Overview. La tua app Next.js servirà allora un robots.txt valido con solo direttive standard.
X-Robots-Tag noindex: se Search Console riporta "Page is not indexed: Excluded by 'noindex' tag" con "'noindex' detected in 'X-Robots-Tag' http header", la direttiva arriva da Cloudflare, non dalla tua app. La tua app Next.js può mandare index, follow, ma Cloudflare lo può sovrascrivere. Controlla Rules → Transform Rules → Modify Response Header per qualunque regola che imposti X-Robots-Tag: noindex. Controlla Security → Bots → Crawl Control (o AI Crawl Control) e disabilita le opzioni che aggiungono noindex. Verifica con curl -I -A "Googlebot" https://your-domain.com/ e conferma che la risposta abbia x-robots-tag: index, follow.
Verifica: apri https://your-domain.com/robots.txt e conferma che contenga la riga Sitemap e nessuna riga Content-Signal.
Tool di verifica
| Tool | URL | Uso |
|---|---|---|
| Google Search Console | search.google.com/search-console | Indicizzazione, sitemap, Core Web Vitals |
| Lighthouse | Chrome DevTools → F12 → tab Lighthouse | Audit SEO, performance, accessibilità |
| PageSpeed Insights | pagespeed.web.dev | Core Web Vitals, performance mobile |
| Schema Validator | validator.schema.org | Validazione JSON-LD |
Check rapido: esegui Lighthouse sulla tua homepage. La categoria SEO deve passare su document title, meta description, link crawlabili e robots.txt.
Checklist
-
NEXT_PUBLIC_SITE_URLimpostato in Coolify -
/sitemap.xmle/robots.txtritornano 200 - URL canonical su tutte le pagine
- JSON-LD su homepage e pagine articolo
- Property Google Search Console verificata
- Sitemap inviata, stato Success
- Cloudflare non blocca Googlebot
- Score SEO Lighthouse accettabile