SEO
Contrat de données SEO, construction des métadonnées Next.js, score de rédaction et résolution des tokens.
Le SEO est une capacité partagée du core, pas un module. Il n'existe pas de mécanisme d'injection de champs d'un module vers un autre, et les données SEO sont par contenu : elles vivent dans la table de chaque module.
Le core fournit donc les briques communes. Chaque module de contenu stocke son objet seo et appelle le builder.
Import
import {
EMPTY_SEO,
buildSeoMetadata, computeSeoScore,
applySeoTokens, resolveSeoTitle, resolveSeoDescription,
isSeoEnabled,
} from '@lrvo/core/seo';Module serveur. Les composants d'édition sont dans @lrvo/core/shared/components.
Activation
LRVO_SEO=trueFlag global, lu côté serveur uniquement. isSeoEnabled() sert au gating de generateMetadata. Côté client, l'éditeur le lit par useLrvoConfig().seo : une variable sans préfixe NEXT_PUBLIC_ n'existe pas dans le bundle navigateur.
Contrat de données
EMPTY_SEO
Forme par défaut de l'objet seo, partagée par tous les modules.
{
title, description, canonical,
keywords: [],
robots: { index: true, follow: true, noarchive: false },
openGraph: { title, description, image: { id, url, alt } },
twitter: { card: 'summary_large_image', title, description },
}Un module stocke cet objet dans une colonne JSONB dédiée, ou dans une clé d'un data JSONB existant.
Tokens
Le titre et la description acceptent des tokens résolus au rendu.
| Token | Valeur |
|---|---|
%title% | Titre du contenu, le fallbackTitle. |
%sitename% | Nom du site, LRVO_NAME. |
%sep% ou %separator% | Séparateur, LRVO_SEO_SEPARATOR, défaut -. |
LRVO_NAME=Mon site
LRVO_SEO_SEPARATOR=-
LRVO_SEO_TITLE_TEMPLATE=%title% %sep% %sitename%Règles de résolution :
- Titre SEO vide : on applique
LRVO_SEO_TITLE_TEMPLATE, ce qui donneMa page - Mon site. - Titre SEO renseigné : il sert lui-même de gabarit et peut contenir des tokens. Sans token, il est pris littéralement.
- Un séparateur orphelin en début ou en fin, laissé par un token vide, est retiré automatiquement.
applySeoTokens(str, { title, siteName, separator }), resolveSeoTitle(seo, ctx) et resolveSeoDescription(seo, ctx) exposent cette résolution, réutilisée par le builder, le score et les aperçus.
Côté serveur, siteName, separator et titleTemplate tombent sur la configuration. Côté client, l'éditeur les passe depuis useLrvoConfig().
buildSeoMetadata(seo, ctx)
Transforme l'objet seo stocké en objet Metadata Next.js complet, prêt à être retourné par un generateMetadata. Les clés vides sont omises : pas de balise inutile dans le <head>.
ctx | Rôle |
|---|---|
path | Chemin public du contenu, /about/team. Sert de canonical par défaut. |
fallbackTitle | Titre du contenu quand il n'y a pas de meta title. |
fallbackDescription | Description de repli. |
fallbackImage | Image de partage de repli, objet { url } ou chaîne. |
ogType | Type Open Graph. Défaut website ; article pour un billet. |
siteName, separator, titleTemplate | Surchargent la configuration pour ce rendu. Utile quand la valeur ne vient pas de l'environnement. |
Sorties : title, description, keywords, alternates.canonical, robots (via generateRobots de @lrvo/core/shared/metadata), openGraph (title, description, url, images, siteName, type) et twitter (card, title, description, images).
Les URL relatives, canonical et images, sont rendues absolues avec getPublicBaseUrl().
import { buildSeoMetadata, isSeoEnabled } from '@lrvo/core/seo';
export async function generateMetadata({ params }) {
const page = await getPublishedPageBySegments(segments);
if (!page) return {};
if (!isSeoEnabled()) return { title: page.title };
return buildSeoMetadata(page.seo, {
path: '/' + segments.join('/'),
fallbackTitle: page.title,
});
}computeSeoScore(seo, ctx)
Calcule un score sur 100 avec le détail des contrôles : longueurs de titre et de description, présence d'image de partage, canonical, indexabilité, texte social, mots-clés.
C'est un indicateur d'aide à la rédaction, pas un audit technique. La fonction est pure et neutre, réutilisable serveur comme client.
const { score, checks } = computeSeoScore(seo, {
fallbackTitle: page.title, // compte quand il n'y a pas de meta title
fallbackDescription: page.excerpt, // compte quand il n'y a pas de meta description
fallbackImage: page.coverUrl, // compte quand il n'y a pas d'image OG
hasPath: true, // une URL publique existe, donc canonical auto
});
// checks: [{ id, label, status: 'pass' | 'warn' | 'fail', points, max, hint }]Les trois clés de repli sont les mêmes que celles de buildSeoMetadata, et suivent la même chaîne de résolution. Passer à l'une la valeur que passe l'autre est la seule façon d'obtenir un score qui reflète ce que la page publique émet réellement, plutôt que ce qui a été saisi à la main.
Paliers de couleur utilisés par l'UI : vert à partir de 80, ambre à partir de 50, rouge en dessous.
Composants d'édition
SeoFields
Zone d'édition complète : champs SEO, jauge de score en temps réel, aperçus du rendu sur Google, X, Facebook, LinkedIn, Pinterest et Slack.
| Prop | Rôle |
|---|---|
value / onChange | Objet seo contrôlé. |
errors | Erreurs par champ. Optionnel. |
sourcePath | Chemin public du contenu. Sert de placeholder au canonical et d'URL d'aperçu. |
fallbackTitle | Titre du contenu. Alimente aperçus et score quand le titre SEO est vide. |
fallbackDescription | Description dérivée du contenu. Alimente aperçus et score quand la méta description est vide, et sert de placeholder au champ. |
fallbackImage | Image dérivée du contenu, chaîne ou objet { url }. Affichée en filigrane et comptée dans le score tant qu'aucune image OG n'est choisie. |
siteName, separator, titleTemplate | Résolution des tokens côté client, depuis useLrvoConfig(). |
Les deux clés de repli ne sont jamais écrites dans l'objet seo : elles servent uniquement à montrer au rédacteur ce que la page publique émettra en l'absence de saisie.
L'image de partage passe par le sélecteur du registre média, qui se masque sans @lrvo/module-medias.
SeoScoreBadge
Pastille compacte pour les tables d'admin. Elle réutilise computeSeoScore en interne, donc le score listé colle exactement à celui de la jauge.
| Prop | Rôle |
|---|---|
seo | Objet seo de la ligne. null est toléré. |
fallbackTitle | Titre du contenu. |
fallbackDescription | Description dérivée du contenu. |
fallbackImage | Image dérivée du contenu, chaîne ou objet { url }. |
hasPath | Une URL publique existe. |
siteName, separator, titleTemplate | Résolution des tokens. |
Adopter le SEO dans un module
- Stocker un objet
seode formeEMPTY_SEO. - Éditer : rendre
<SeoFields>sous la zone de contenu, conditionné paruseLrvoConfig().seo. - Lister : ajouter une colonne
<SeoScoreBadge>à la table d'admin, conditionnée de la même façon. La requête de liste doit alors sélectionner la colonneseo. - Rendre : appeler
buildSeoMetadatadans legenerateMetadatadu module. - Retomber sur le contenu, si le module a de quoi le faire : passer les mêmes
fallbackDescriptionetfallbackImageaux quatre appels ci-dessus, pour qu'éditeur, liste et rendu public disent la même chose.@lrvo/module-postsles dérive des champs déclarés de chaque type.
Le pas à pas complet est dans SEO et métadonnées.