Chaque page de votre site Next.js partage peut-être le lien de l'accueil

Next fusionne les métadonnées de page dans celles du layout, et c'est sur openGraph que cela dérape. Trois bugs de métadonnées invisibles depuis votre propre site, et l'unique helper qui les referme tous.

Celui-ci est resté invisible depuis l'intérieur du site pendant des semaines. Chaque page s'affichait correctement, chaque titre était bon dans l'onglet, et rien dans le build ne se plaignait. Le bug n'est apparu que lorsque quelqu'un a collé un lien dans Slack.

La fusion se fait par champ, et openGraph est un champ

Next fusionne les métadonnées d'une route dans celles de son layout parent. Ce mot, fusionner, fait beaucoup de travail silencieux, car la fusion se produit au niveau supérieur de l'objet et non à l'intérieur. Une page qui n'exporte qu'un titre et une description hérite donc de tout l'objet openGraph du layout tel quel, og:url compris.

Sur notre site, cela signifiait que chaque page de service et chaque étude de cas expédiait un og:url pointant vers l'accueil, avec les og:title et og:description de l'accueil. Partagée sur LinkedIn ou Slack, une étude de cas affichait sa propre image au-dessus du titre et du lien de l'accueil. L'image était juste, et c'est précisément pour cela que personne n'a regardé deux fois. Ces images sont générées au build : c'était donc la seule partie que personne ne soupçonnait.

Articles

Vous voulez la même chose sur votre site ?

Réserver un appel

Le correctif qui aggrave les choses

La réaction évidente est de surcharger openGraph sur la page et d'y mettre le bon titre. C'est ce qu'ont fait nos articles de blog, et cela a introduit un second bug : la surcharge remplace tout l'objet au lieu d'y fusionner, si bien que ces pages ont corrigé og:title et supprimé og:url entièrement.

// layout.tsx
export const metadata = {
  openGraph: {
    url: "https://example.com/en",
    title: "Homepage title",
    description: "Homepage description",
    images: ["/en/opengraph-image"],
  },
};

// page.tsx  — inherits the layout's ENTIRE openGraph object.
// og:url is the homepage. So is og:title.
export const metadata = {
  title: "Case study",
  description: "What we built",
};

// page.tsx  — the "fix". Replaces the object outright:
// og:url and og:images are now gone.
export const metadata = {
  openGraph: { title: "Case study" },
};
Les deux semblent corrects en relecture et aucun ne l'est. Le premier hérite trop, le second remplace trop.

Définissez les quatre ensemble, au même endroit

La seule version qui reste corrigée est un helper unique appelé par chaque route, qui émet toujours ensemble le titre, la description, la canonique et l'objet openGraph complet. Non parce qu'un helper est plus propre, mais parce que le mode de défaillance est un objet partiel, et qu'un helper est la seule chose qui rende un objet partiel impossible à exprimer.

Tant que vous y êtes : cessez d'écrire les descriptions deux fois

Le bug voisin, c'est une chaîne qui fait deux métiers. Un paragraphe de service écrit pour être lu sur la page atteignait 231 caractères en meta description, et Google le coupait en milieu de proposition. La réponse n'est pas de raccourcir le paragraphe. C'est d'en dériver l'extrait : autant de phrases entières que possible, pour qu'il se termine là où l'auteur s'est arrêté, pas à un décompte de caractères.

const MAX_DESC = 158;

export function metaDescription(text: string, max = MAX_DESC): string {
  const clean = text.replace(/\s+/g, " ").trim();
  if (clean.length <= max) return clean;

  // Keep the ender attached to the sentence it closes. The Arabic full
  // stop and question mark are in the class because the same helper runs
  // on all three locales, and a Latin-only split silently returns the
  // whole paragraph on an Arabic page.
  const sentences = clean.match(/[^.!?۔؟]+[.!?۔؟]*\s*/g) ?? [clean];

  let out = "";
  for (const s of sentences) {
    if (out && (out + s).trim().length > max) break;
    out += s;
  }
  out = out.trim();
  if (out) return out;

  // One sentence longer than the limit: trim at the last whole word.
  const cut = clean.slice(0, max - 1);
  return cut.slice(0, cut.lastIndexOf(" ")).trim() + "…";
}
Le détail multilingue compte plus qu'il n'y paraît. Un découpeur de phrases qui ne connaît que la ponctuation latine ne lève pas d'erreur sur l'arabe : il ne correspond simplement jamais, et renvoie tout le paragraphe.

Les titres appellent le même traitement pour une autre raison. Ajouter aveuglément un suffixe de marque nous a donné About | The team behind Growna | Growna sur douze pages, car plusieurs titres portaient déjà la marque. Et quand le suffixe ne tient pas dans la soixantaine de caractères que Google affiche, le supprimer vaut mieux que le tronquer : un nom de marque coupé en fin de résultat se lit plus mal que pas de marque du tout, et le domaine s'affiche à côté dans les deux cas.

Comment vérifier réellement

Aucun de ces bugs n'est visible dans un navigateur, et c'est toute la raison de leur survie. Récupérez le HTML brut et lisez les balises. Faites-le sur une route enfant, pas sur l'accueil, car l'accueil est la seule page où hériter de l'openGraph du layout se trouve être correct.

# Read what you actually ship, from a child route.
curl -s https://example.com/en/work/some-case-study \
  | grep -oE '<meta property="og:[^"]*" content="[^"]*"|<link rel="canonical"[^>]*>'

# What you want to see:
#   og:url        the CHILD url, not the homepage
#   og:title      the child's own title
#   og:image      the child's own image
#   canonical     self-referencing, and matching og:url
Ajoutez-le à la CI sur une poignée de routes représentatives. Une vérification de deux lignes qui attrape une classe de bugs qu'aucune navigation locale ne vous montrera.

Construisons le site que votre entreprise mérite.

Envoyez ce que vous avez aujourd'hui et où vous voulez aller. Vous obtenez une réponse claire sur le périmètre, le calendrier et le budget, généralement dans l'heure.

Nous travaillons en Amérique du Nord, en Europe et au Moyen-Orient.