Next.js: Eine Seite, die openGraph setzt, verliert das og:image des Layouts

Christian Drexler

Bei einer Prüfung dieser Seite habe ich mir angesehen, was jede Unterseite anbietet, wenn ihr Link geteilt wird. Die Startseite hatte ein Vorschaubild. Die Leistungsseiten hatten einen Titel und sonst nichts, also die Art Link, die im Chat als kahler grauer Kasten erscheint. Dieselbe Seite, dasselbe Layout, dasselbe Deployment.

Das Vorschaubild war genau einmal definiert, im Root-Layout, wo es hingehört. Die Startseite hatte es. Fast nichts sonst.

Was passiert

Im App Router kann jedes Layout und jede Seite metadata oder generateMetadata exportieren. Next.js führt sie von der Wurzel bis zur Seite zusammen. Entscheidend ist, wie tief das geht: Es geht nur eine Ebene tief. Schlüssel der obersten Ebene werden zusammengeführt, und wenn eine Seite einen Schlüssel definiert, den auch das Layout definiert, ersetzt der Wert der Seite den des Layouts vollständig.

openGraph ist so ein Schlüssel. Mit diesem Layout:

// app/layout.tsx
export const metadata = {
  openGraph: {
    siteName: "Example",
    images: [{ url: "/og.jpg", width: 1200, height: 630 }],
  },
};

und dieser Seite:

// app/services/page.tsx
export const metadata = {
  openGraph: { title: "Services", url: "https://example.com/services" },
};

hat die ausgelieferte Seite einen Open-Graph-Titel und eine Adresse, aber kein Bild und keinen Seitennamen. Die Seite hat dem Objekt des Layouts keine zwei Felder hinzugefügt. Sie hat das Objekt ersetzt.

Die Startseite funktionierte, weil sie kein eigenes openGraph definierte und das des Layouts unberührt erbte. Jede Seite, bei der ich sorgfältig genug war, einen ordentlichen Titel und eine Adresse zu setzen, hatte zum Dank das Bild verloren.

Dieselbe Regel gilt für twitter, robots und alternates. Jedes verschachtelte Objekt, das eine Seite definiert, steht für sich allein.

Wie man es sieht

Der Quelltext lügt weniger als jedes Vorschau-Werkzeug, und curl ist schneller als beide.

for p in / /services /blog /about; do
  printf "%-12s " "$p"
  curl -s "https://example.com$p" | grep -o '<meta property="og:image"[^>]*>' | head -1
  echo
done

Eine Zeile mit einem Pfad und nichts dahinter ist eine Seite ohne Vorschaubild. Ich habe das über die ganze Sitemap laufen lassen, und das Ergebnis war leicht zu lesen: Beiträge und Werkzeug-Seiten hatten ein Bild, weil diese Seiten ihr eigenes Titelbild setzen. Alles andere war leer.

Deshalb hatte der Fehler auch überlebt. Die Seiten, die am häufigsten geteilt werden, die Beiträge, waren in Ordnung.

Die Lösung

Einen Schalter für tiefes Zusammenführen gibt es nicht. Die Seite muss das vollständige Objekt mitbringen. Damit daraus kein Kopieren und Einfügen wird, liegt der gemeinsame Teil in einer Konstante, und jede Seite nimmt ihn auf.

// lib/seo.ts
export const OG_IMAGES = [{ url: "/og.jpg", width: 1200, height: 630 }];
// app/services/page.tsx
import { OG_IMAGES } from "@/lib/seo";

export const metadata = {
  openGraph: {
    images: OG_IMAGES,
    title: "Services",
    url: "https://example.com/services",
  },
};

Seiten mit einem besseren eigenen Bild, etwa ein Beitrag mit Titelbild, übergeben stattdessen dieses. Die Konstante ist der Rückfall, und jetzt ein ausdrücklicher.

Wer lieber echte Vererbung will: generateMetadata bekommt die aufgelösten Metadaten des Elternteils als zweites Argument, und von dort lassen sich die Bilder lesen.

export async function generateMetadata(_: unknown, parent: ResolvingMetadata) {
  const previous = (await parent).openGraph?.images ?? [];
  return { openGraph: { images: previous, title: "Services" } };
}

Das funktioniert, und es ist das, was die Dokumentation vorschlägt. Ich habe die Konstante gewählt, weil sie ein Import ist, weil sie auf einer Seite mit statischem metadata-Export nicht stillschweigend vergessen werden kann und weil mir eine Suche nach OG_IMAGES jede Seite zeigt, die sie hat.

Eine Prüfung daraus machen

Ein fehlendes Vorschaubild lässt keinen Build scheitern, taucht in keinem Lighthouse-Wert auf und wirft im Browser nichts aus. Man findet es, wenn ein Link im Chatfenster von jemandem kahl aussieht, oder wenn man danach sucht. Die curl-Schleife von oben steht deshalb auf der kurzen Liste der Dinge, die ich gegen ein Vorschau-Deployment laufen lasse, bevor es Produktion wird, neben Titel und Canonical.

Drei Zeilen Shell über die Sitemap:

curl -s https://example.com/sitemap.xml | grep -o '<loc>[^<]*' | sed 's/<loc>//' |
while read -r u; do
  curl -s "$u" | grep -q 'property="og:image"' || echo "no og:image: $u"
done

Keine Ausgabe heißt, jede Seite hat eines.

Die allgemeine Lehre

Das Layout fühlt sich wie der Ort an, an dem gemeinsame Metadaten leben sollten, und für einfache Schlüssel wie title-Vorlagen oder metadataBase stimmt das. Für verschachtelte Objekte liefert das Layout nur einen Vorgabewert für Seiten, die nichts sagen. Sobald eine Seite etwas sagt, muss sie alles sagen.

Die Lösung war eine Konstante und eine Zeile in jeder Seitendatei. Die Prüfung, die es beim nächsten Mal findet, sind drei Zeilen Shell.