SEO
Métadonnées par page, sitemap construit depuis l'arbre des pages publiées, et fichier robots qui garde la route d'édition hors de l'index.
Les champs SEO sont du contenu : ils appartiennent donc aux éditeurs. Le rôle de votre application est de les lire et de donner à Next.js les bonnes formes.
Métadonnées de page
Chaque page porte seoTitle, seoDescription, seoKeywords et displayName, tous multilingues. buildCmssyMetadata les transforme en objet Metadata de Next.js :
// app/[[...path]]/page.tsx
import { buildCmssyMetadata } from "@cmssy/next/server";
import { cmssy } from "@/cmssy.config";
export async function generateMetadata({ params }) {
const { path } = await params;
// Passez le chemin tel que routé, préfixe compris : le préfixe EST la langue.
return buildCmssyMetadata(cmssy, path);
}Il résout la locale depuis le chemin, récupère les champs SEO de la page et renseigne l'URL canonique, les alternatives hreflang, Open Graph et la carte Twitter depuis le branding de votre configuration de site. Les champs laissés vides retombent sur le nom d'affichage, puis sur le nom du site : une page a donc toujours un titre, même inachevée.
Sitemap et robots sont vos routes
cmssy ne fournit pas de helper de sitemap, et c'est le modèle headless qui fonctionne comme prévu. Un sitemap, c'est une requête plus une mise en forme adaptée à votre framework : le CMS n'a rien à faire dans votre fichier de route.
L'arbre des pages publiées est déjà le sitemap. Interrogez-le, transformez-le :
// app/sitemap.ts
import { listPublicPages } from "@/services/pages";
import { fetchSiteConfig, resolveSiteLocales } from "@/services/site";
import { localizedPath } from "@/lib/locale-path";
export const dynamic = "force-dynamic";
export default async function sitemap() {
const [{ defaultLocale, locales }, pages, siteConfig] = await Promise.all([
resolveSiteLocales(),
listPublicPages(),
fetchSiteConfig(),
]);
const notFoundPageId = siteConfig?.notFoundPageId ?? null;
return pages
.filter((page) => page.publishedAt && page.id !== notFoundPageId)
.map((page) => ({
url: `${SITE_URL}${localizedPath(page.slug, defaultLocale, defaultLocale)}`,
lastModified: new Date(page.updatedAt ?? page.publishedAt),
}));
}Deux filtres, deux raisons distinctes. publishedAt : list renvoie aussi les brouillons, qui n'ont rien à faire dans un sitemap. notFoundPageId : la page 404 est publiée comme les autres, et la lister invite les crawlers à indexer une erreur - l'espace de travail dit déjà laquelle c'est, via siteConfig.
Gardez-le comme helper, pas comme corps de route
Mettez la requête et la transformation dans services/ et laissez le fichier de route à quatre lignes. Les produits ou catégories issus d'enregistrements ne sont pas des pages : l'arbre ne les connaît pas. Quand vous les ajouterez, vous voudrez une seule fonction propriétaire de la forme des URL et du hreflang, plutôt que deux endroits pouvant diverger sur le domaine.
C'est aussi ce qui rend le motif portable : le même helper, avec une autre forme de retour, alimente une application Astro ou Remix. La requête est la partie réutilisable, la route n'est que l'adaptateur.
Robots
// app/robots.ts
export const dynamic = "force-dynamic";
export default function robots() {
return {
rules: {
userAgent: "*",
allow: "/",
disallow: ["/cmssy-edit/", "/api/"],
},
sitemap: `${SITE_URL}/sitemap.xml`,
};
}Interdire /cmssy-edit/ n'est pas optionnel. Cette route sert du contenu brouillon et monte l'éditeur. Indexée, elle placerait des textes non publiés dans les résultats de recherche et ferait remonter un doublon de chacune de vos pages.
Les deux routes doivent être dynamiques
export const dynamic = "force-dynamic";Sitemap et robots lisent l'état vivant du CMS. Généré statiquement à la compilation, votre sitemap se fige au jour du déploiement et cesse discrètement de lister tout ce qui a été publié depuis.
Étapes suivantes
- i18n - comment les locales façonnent les URL et le hreflang.
- Routes et pages - où vit
generateMetadata. - Branding - la configuration derrière les images Open Graph.