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=true

Flag 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.

TokenValeur
%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 donne Ma 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>.

ctxRôle
pathChemin public du contenu, /about/team. Sert de canonical par défaut.
fallbackTitleTitre du contenu quand il n'y a pas de meta title.
fallbackDescriptionDescription de repli.
fallbackImageImage de partage de repli, objet { url } ou chaîne.
ogTypeType Open Graph. Défaut website ; article pour un billet.
siteName, separator, titleTemplateSurchargent 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.

PropRôle
value / onChangeObjet seo contrôlé.
errorsErreurs par champ. Optionnel.
sourcePathChemin public du contenu. Sert de placeholder au canonical et d'URL d'aperçu.
fallbackTitleTitre du contenu. Alimente aperçus et score quand le titre SEO est vide.
fallbackDescriptionDescription dérivée du contenu. Alimente aperçus et score quand la méta description est vide, et sert de placeholder au champ.
fallbackImageImage 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, titleTemplateRé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.

PropRôle
seoObjet seo de la ligne. null est toléré.
fallbackTitleTitre du contenu.
fallbackDescriptionDescription dérivée du contenu.
fallbackImageImage dérivée du contenu, chaîne ou objet { url }.
hasPathUne URL publique existe.
siteName, separator, titleTemplateRésolution des tokens.

Adopter le SEO dans un module

  1. Stocker un objet seo de forme EMPTY_SEO.
  2. Éditer : rendre <SeoFields> sous la zone de contenu, conditionné par useLrvoConfig().seo.
  3. 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 colonne seo.
  4. Rendre : appeler buildSeoMetadata dans le generateMetadata du module.
  5. Retomber sur le contenu, si le module a de quoi le faire : passer les mêmes fallbackDescription et fallbackImage aux quatre appels ci-dessus, pour qu'éditeur, liste et rendu public disent la même chose. @lrvo/module-posts les dérive des champs déclarés de chaque type.

Le pas à pas complet est dans SEO et métadonnées.

Sur cette page