Öffentlichen Inhalt, Metadaten und strukturierte Daten gemeinsam implementieren
Erzeugen Sie Inhalt, Titel, kanonische URL und strukturierte Daten aus dem Text einer öffentlichen Abfragefunktion. Vergleichen Sie im selben Beispiel SSR pro Anfrage mit beim Build erzeugten Archivseiten.
Konfigurationsorte bestimmen und Web, Server und Infrastruktur verbinden
Ziel ist ein öffentlicher Nutzungshinweis mit übereinstimmendem Titel, Inhalt, Adresse, Daten und Belegen. Die Suche kann Thema und kanonische Adresse verarbeiten; webgestützte Antworten können Fakten und Bedingungen nutzen. Neue Zeiten im Inhalt, aber alte in Zusammenfassung und JSON-LD erzeugen Widersprüche. Der folgende Code leitet deshalb alle Ausgaben aus derselben veröffentlichten Revision ab.
Bestimmen Sie zuerst, wann öffentliche Seiten wechseln. Selten geänderte Hinweise können beim Build entstehen; häufig geänderte Artikel lesen serverseitig die aktuelle Revision oder aktualisieren Caches bei Veröffentlichung. Öffentliche Seiten und Verwaltung können eine Anwendung teilen, behalten aber getrennte Abfrageberechtigungen und Cache-Bedingungen.
Die folgende Dateianordnung ist ein Beispiel für eine Next.js-Website mit CMS. In Repositories mit src liegen app und lib darunter. Funktionsnamen wie getPublicGuide werden passend zum Projekt gewählt.
app/
layout.tsx Gemeinsame Oberfläche und Standardmetadaten
guides/[slug]/page.tsx Öffentliche Abfrage, Inhalt, Detailmetadaten
robots.ts Crawling-Hinweise
sitemap.ts Öffentliche kanonische URLs
lib/
guides.ts Veröffentlichungsbedingungen und Abfragen
identity-time.mjs Daten, Öffentlichkeit, kanonische URLs
site.ts Produktionsdomain
next.config.ts Framework- und Bereitstellungskonfiguration
package.json Pakete sowie build- und start-Befehle
Build-Befehle stehen in scripts der package.json. Ein direkt betriebener Node.js-Server kann mit next build erzeugt und mit next start gestartet werden. Statische Exporte kommen auf statisches Hosting. Verwaltete Plattformen nutzen die Build- und Ausführungseinstellungen des Projekts; prüfen Sie Ausgaben und unterstützte Serverfunktionen.
Verbinden Sie die Produktionsdomain über DNS und Zertifikat und lassen Sie öffentliche Pfade durch CDN und Firewall. Der Server liest öffentliche Artikel und liefert HTML, Metadaten und Sitemap. Prüfen Sie anschließend Cache-Updates durch Veröffentlichungsereignisse. Dieselbe URL über Verbindung bis Inhalt anzufragen macht abweichende Einstellungen leichter auffindbar.
Versionen und Beispieldaten festlegen
Das App-Router-Beispiel läuft mit Next.js 16.3.8, React 19.2.5, TypeScript 5.9.3 und Node.js 22. cacheComponents wird in next.config.ts auf false gesetzt, um die folgenden dynamic-Einstellungen zu verwenden. Mit aktivierten Cache Components ist eine separate Konfiguration über use cache, cacheLife und die zugehörigen Optionen erforderlich.
Die Daten bestehen aus einem öffentlichen Bibliothekshinweis und einem Entwurf. Ein festes Array macht Abfragen und Ausgabe ohne Netzwerk oder reale Datenbank nachvollziehbar. Im Betrieb wird das Arraylesen durch eine Server-Datenbankabfrage mit denselben Öffentlichkeitsbedingungen ersetzt.
Speichern Sie die Dateien mit den angegebenen Namen in einem leeren Verzeichnis. Erstellen Sie die Seiten- und Abfragedateien dieses Kapitels sowie die späteren robots- und Sitemap-Dateien, bevor Sie installieren und bauen. Next.js kann die TypeScript-Konfiguration beim ersten Start erzeugen. Vergleichen Sie bei bestehenden Projekten zuerst installierte Versionen und Einstellungen.
Die nächsten drei Blöcke sind package.json, next.config.ts und Terminalbefehle. Verwenden Sie Node.js 22 und behalten Sie nach der Installation package-lock.json. Zur Reproduktion der Umgebung gehören angegebene Versionen und Lockdatei zusammen.
{
"name": "seo-guide-sample",
"private": true,
"scripts": {
"build": "next build --webpack",
"start": "next start",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next": "16.3.8",
"react": "19.2.5",
"react-dom": "19.2.5"
},
"devDependencies": {
"typescript": "5.9.3",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19"
}
}
import type { NextConfig } from 'next'
const config: NextConfig = { cacheComponents: false }
export default config
npm install
npm run build
npm run start -- --port 3027
npm install installiert deklarierte Pakete, build erstellt die Produktionsausgabe, start führt sie aus. Die Übung läuft auf http://localhost:3027; example.com ist die exemplarische kanonische Bereitstellungsadresse. Setzen Sie SITE_ORIGIN im Betrieb auf die bestätigte Domain.
Öffentliche Abfrage und Wiederverwendung innerhalb einer Anfrage implementieren
Die kanonische Domain steht in lib/site.ts, Bedingungen und Abfragen in lib/guides.ts. React cache verwendet Ergebnisse gleicher Argumente während derselben Serverrendering-Anfrage wieder. Die Lebensdauer unterscheidet sich von CDN- und Datencaches über mehrere Besuche.
export const SITE_ORIGIN = 'https://example.com'
import { cache } from 'react'
import { describeDocument } from './identity-time.mjs'
import { SITE_ORIGIN } from './site'
export type Guide = {
id: string
previousSlugs: string[]
publicFrom: string | null
slug: string
title: string
summary: string
paragraphs: string[]
sourceUrl: string | null
validFrom: string | null
validUntil: string | null
status: 'draft' | 'published' | 'withdrawn'
publishedAt: string | null
contentModifiedAt: string | null
deletedAt: string | null
revision: number
}
// Feste Übungsdaten. Im Betrieb durch Datenbankabfragen ersetzen.
const guides: Guide[] = [
{
id: 'hours-current', previousSlugs: ['library-hours'],
publicFrom: '2026-10-01T00:00:00+09:00',
slug: 'opening-hours', title: 'Öffnungszeiten der Bibliothek',
summary: 'Sonntagsöffnungszeiten und Schließtage.',
paragraphs: [
'Sonntags von 10 bis 17 Uhr geöffnet.',
'Am ersten Sonntag jedes Monats und an Feiertagen geschlossen. Dieser Zeitplan gilt ab Oktober 2026.',
],
sourceUrl: '/policies/opening-hours',
validFrom: '2026-10-01T00:00:00+09:00', validUntil: null,
status: 'published', publishedAt: '2026-10-01',
contentModifiedAt: '2026-10-23T09:00:00+09:00',
deletedAt: null, revision: 2,
},
{
id: 'draft-example', previousSlugs: [], publicFrom: null,
slug: 'draft-guide', title: 'Unveröffentlichte Anleitung', summary: 'Entwurf',
paragraphs: ['Übungsentwurf, der nicht in öffentlichen Antworten erscheinen darf.'],
sourceUrl: null, validFrom: null, validUntil: null,
status: 'draft', publishedAt: null, contentModifiedAt: null,
deletedAt: null, revision: 1,
},
]
function toPublic(g: Guide, asOf: string) {
const info = describeDocument({ ...g, path: `/guides/${g.slug}`, eventDate: null }, asOf, SITE_ORIGIN)
return info ? { ...g, info } : null
}
// Inhalt und metadata einer Anfrage lesen dieselbe Zeit und öffentliche Revision.
const snapshot = cache(async () => {
const asOf = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z')
return guides.map(g => toPublic(g, asOf)).filter(g => g !== null)
})
export const getPublicGuide = cache(async (slug: string) => {
return (await snapshot()).find(g => g.slug === slug || g.previousSlugs.includes(slug)) ?? null
})
export const listPublicGuides = snapshot
lib/guides.ts liest feste Daten und berechnet mit describeDocument ID, canonical, Daten, Öffentlichkeit und Gültigkeit. React cache an snapshot verwendet das Ergebnis einer Serveranfrage wieder; getPublicGuide und listPublicGuides teilen es. Der Umfang unterscheidet sich von langfristigen DB- und CDN-Caches. React cache
sourceUrl ist ein tatsächlich erreichbarer Belegpfad und verweist hier auf das unten erzeugte /policies/opening-hours. validFrom und validUntil beschreiben die Gültigkeit des Zeitplans unabhängig von publicFrom als Beginn der Serverfreigabe.
Speichern Sie zunächst identity-time.mjs aus Dokumente und Zeit als lib/identity-time.mjs. guides.ts ruft describeDocument tatsächlich auf. snapshot legt die Anfragezeit einmal fest und liefert öffentliche Texte samt Beschreibung. Auch der alte slug library-hours findet dasselbe Dokument, doch page und generateMetadata rufen permanentRedirect auf und senden 308 nach opening-hours. @id entsteht aus hours-current und bleibt bei Adressänderungen stabil.
SITE_ORIGIN wird auch als drittes Argument an describeDocument übergeben. Kanonische Adresse und stabile @id entstehen aus derselben Einstellung, sodass bei Domainwechseln die Standardbeispieladresse im Datumsmodul nicht separat geändert werden muss.
Das gemeinsame snapshot reduziert doppelte Abfragen und Inhaltswidersprüche. Ändert sich die öffentliche Revision zwischen Titel- und Inhaltsabfrage, könnten verschiedene Fassungen in einer Antwort stehen. Das Beispiel verwendet die innerhalb der Anfrage bestimmte öffentliche Liste wieder, sodass Titel, Inhalt und Datum dieselben Daten beschreiben. Dadurch bleibt die Bedeutung des an die Suche gelieferten Dokuments konsistent.
Dieselbe öffentliche Revision auf der SSR-Detailseite ausgeben
app/layout.tsx legt Dokumentsprache und Basis-URL fest. app/guides/[slug]/page.tsx liest das öffentliche Material für den slug. force-dynamic wählt anfragebezogenes Rendering in dieser Konfiguration mit deaktivierten Cache Components.
import type { Metadata } from 'next'
import type { ReactNode } from 'react'
import { SITE_ORIGIN } from '../lib/site'
export const metadata: Metadata = { metadataBase: new URL(SITE_ORIGIN) }
export default function Layout({ children }: { children: ReactNode }) {
return <html lang="de"><body>{children}</body></html>
}
import type { Metadata } from 'next'
import { notFound, permanentRedirect } from 'next/navigation'
import { getPublicGuide } from '../../../lib/guides'
import { SITE_ORIGIN } from '../../../lib/site'
import { formatContentDate } from '../../../lib/identity-time.mjs'
export const dynamic = 'force-dynamic'
type Props = { params: Promise<{ slug: string }> }
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const guide = await getPublicGuide(slug)
if (!guide) notFound()
if (slug !== guide.slug) permanentRedirect(`/guides/${guide.slug}`)
const canonical = guide.info.canonical
return {
title: guide.title, description: guide.summary,
alternates: { canonical },
robots: { index: true, follow: true },
openGraph: { title: guide.title, description: guide.summary, url: canonical },
}
}
export default async function Page({ params }: Props) {
const { slug } = await params
const guide = await getPublicGuide(slug)
if (!guide) notFound()
if (slug !== guide.slug) permanentRedirect(`/guides/${guide.slug}`)
const canonical = guide.info.canonical
const structured = {
'@context': 'https://schema.org', '@type': 'WebPage',
'@id': guide.info.id, url: canonical,
name: guide.title, description: guide.summary,
...(guide.info.datePublished ? { datePublished: guide.info.datePublished } : {}),
...(guide.sourceUrl ? { citation: new URL(guide.sourceUrl, SITE_ORIGIN).href } : {}),
...(guide.info.dateModified ? { dateModified: guide.info.dateModified } : {}),
}
const format = formatContentDate
return <main><article data-revision={guide.revision}>
<h1>{guide.title}</h1><p>{guide.summary}</p>
{guide.publishedAt && <p data-date="datePublished"><span data-label>Veröffentlicht</span> <time dateTime={guide.publishedAt}>
{format(guide.publishedAt)}</time></p>}
{guide.contentModifiedAt && <p data-date="dateModified"><span data-label>Geändert</span> <time dateTime={guide.contentModifiedAt}>
{format(guide.contentModifiedAt)}</time></p>}
<p>Gültigkeit: {({ current: "Aktuell", historical: "Historisch", scheduled: "Geplant", unknown: "Beginn unbekannt" })[guide.info.applicability]}</p>
{guide.validFrom && <p>Gültig ab <time dateTime={guide.validFrom}>
{format(guide.validFrom)}</time></p>}
{guide.validUntil && <p>Gültig bis <time dateTime={guide.validUntil}>
{guide.validUntil}</time> (dieser Zeitpunkt ausgeschlossen)</p>}
{guide.paragraphs.map((text, i) => <p key={i}>{text}</p>)}
{guide.sourceUrl && <p>Beleg: <a href={guide.sourceUrl}>Ursprünglicher Hinweis zu Bibliotheksöffnungszeiten</a></p>}
<script type="application/ld+json" dangerouslySetInnerHTML={{
__html: JSON.stringify(structured).replace(/</g, '\\u003c'),
}} />
</article></main>
}
generateMetadata liefert Titel, Beschreibung und canonical; Page gibt Absätze derselben Daten aus. data-revision kennzeichnet die Ausgabeversion für den Vergleich. Die Anleitung verwendet WebPage. Artikeldienste wählen passende Typen wie Article samt tatsächlichen Autoren, Daten und Bildern.
Fehlen öffentliche Daten, wird notFound aufgerufen. Ladegrenzen oder Streaming können Statuscodes ändern; prüfen Sie deshalb tatsächliche 404-, noindex- und Inhaltsantworten. Fehler der Originalabfrage werden getrennt von fehlenden Artikeln als Serverfehler verfolgt.
Der erste Block ist app/layout.tsx, der zweite app/guides/[slug]/page.tsx. Der angefragte slug geht in die Abfrage; aus demselben guide entstehen canonical, Titel, Inhalt, Daten und Beleglink. format zeigt bestätigte Daten im Format dieser Sprachversion. dateTime bewahrt ursprüngliche Präzision als reines Datum oder Zeitwert mit Zone. contentModifiedAt verändert sichtbares Änderungsdatum und JSON-LD dateModified gemeinsam.
Nach dem Start muss /guides/opening-hours Zeiten, Schließung am ersten Sonntag, Veröffentlichungs- und Änderungsdaten sowie Beleglink zeigen. Fehlen Daten, prüfen Sie null-Werte und time-Zweige. Fehlen Bedingungen trotz JSON-LD im Text, korrigieren Sie paragraphs. Unbekannte slugs und draft-guide müssen 404 und noindex liefern.
Veröffentlichungs- und Änderungszeilen besitzen data-date, Labels data-label. Die Prüfung vergleicht Zeilenbedeutung, time datetime und sichtbares Datum getrennt. info.applicability erscheint ebenfalls, damit ein historischer Eintrag mit neuer Revision nicht aktuell wirkt. Reine publishedAt-Daten zeigt formatContentDate ohne Zeitzonenverschiebung.
Das map für p-Elemente verwandelt Bibliothekszeiten in sichtbares HTML. generateMetadata liefert separat Thema und canonical. Wegen dieser Arbeitsteilung müssen nicht sämtliche Zeiten und Ausnahmen im title stehen. Die Antwort gehört vollständig in den Text, Titel und Beschreibung fassen sie korrekt zusammen. Prüfen Sie nach Änderungen, ob body und head dieselbe Anleitung beschreiben.
Detailartikel aus einer öffentlichen Liste auffindbar machen
Auch die Liste verwendet dieselben Öffentlichkeitsbedingungen. Link in app/page.tsx erzeugt tatsächliche Detailadressen. Entwürfe dürfen weder in der Liste stehen noch bei direktem Aufruf öffentlichen Inhalt liefern.
import Link from 'next/link'
import { listPublicGuides } from '../lib/guides'
export const dynamic = 'force-dynamic'
export default async function Home() {
const guides = await listPublicGuides()
return <main><h1>Bibliothekshinweise</h1><ul>
{guides.map(g => <li key={g.slug}>
<Link href={`/guides/${g.slug}`}>{g.title}</Link>
</li>)}
</ul></main>
}
Durchlaufen Sie guides mit map und erzeugen Sie Link mit echten href-Werten. Speichern Sie als app/page.tsx und öffnen Sie über „Öffnungszeiten der Bibliothek“ die Detailseite. Erscheinen unveröffentlichte Hinweise, prüfen Sie listPublicGuides. Listenweg und Direktaufruf müssen beide geprüft werden, um nur oberflächlich versteckte Entwürfe zu erkennen.
Gibt Link auf der Startseite ein echtes href aus, können Crawler die Detailadresse entdecken. Bei Artikeln, die nur über Suchfeldeingaben oder Klickereignisse öffnen, bleibt der Pfad für Abrufe ohne diese Aktionen möglicherweise unsichtbar. Listenlinks und Sitemap schaffen gemeinsam Wege zur Entdeckung neuer Artikel. Crawlbare Links
Einen festen historischen Eintrag als statische Seite erstellen
Für den einzelnen Archiveintrag der Übung wird app/archive/opening-2025/page.tsx angelegt. Fester Dateiinhalt ohne Anfrageabfrage ergibt eine statische Seite im Build. /archive/missing hat keine Route und fällt auf die normale 404-Behandlung zurück.
import type { Metadata } from 'next'
import { SITE_ORIGIN } from '../../../lib/site'
import { describeDocument, example } from '../../../lib/identity-time.mjs'
// Erstveröffentlichung und Änderung des Archivs sind unbekannt; keine unbelegten Daten ausgeben.
const info = describeDocument({ ...example, publishedAt: null, contentModifiedAt: null },
'2026-10-24T09:00:00+09:00', SITE_ORIGIN)!
export const metadata: Metadata = {
title: 'Aufzeichnung der Bibliotheksöffnungszeiten 2025',
alternates: { canonical: info.canonical },
}
export default function Archive() {
const structured = { '@context': 'https://schema.org', '@type': 'WebPage',
'@id': info.id, url: info.canonical, name: 'Aufzeichnung der Bibliotheksöffnungszeiten 2025' }
return <main><article>
<h1>Aufzeichnung der Bibliotheksöffnungszeiten 2025</h1>
<p>Dieser fiktive Zeitplan galt vom 1. Januar bis 31. Dezember 2025.</p>
<p>Damals war sonntags von 11 bis 16 Uhr geöffnet.</p>
<p>Gültigkeit: {info.applicability === 'historical' ? 'Historisch' : 'Gültigkeitszeitraum prüfen'}</p>
<p>Erstveröffentlichungs- und Änderungsdatum dieses Materials sind unbekannt.</p>
<a href="/guides/opening-hours">Aktuelle Öffnungszeiten und Schließtage</a>
<script type="application/ld+json" dangerouslySetInnerHTML={{
__html: JSON.stringify(structured).replace(/</g, '\\u003c'),
}} />
</article></main>
}
Der Dateipfad bestimmt die historische URL; metadata erzeugt ein selbstreferenzierendes canonical. Der Inhalt erhält den Zeitraum 2025 und verlinkt die aktuelle Anleitung. publishedAt und contentModifiedAt sind null; entsprechende Datumsanzeigen werden übersprungen.
Prüfen Sie im Build, ob /archive/opening-2025 statisch und /guides/[slug] anfragegeneriert ist. Änderungen an festen Dateien verlangen einen neuen Build. Bei vielen Archiven kann generateStaticParams die Pfade liefern. Prüfen Sie dabei unbekannte slugs, neue Dokumente, Rücknahme und Regenerierung separat. Statische Routen erzeugen
Das Archiv ruft describeDocument mit dem example aus „Dokumente und Zeit“ und unbekannten Veröffentlichungs- und Änderungsdaten auf. Unbekannte Werte fehlen sowohl im Text als auch in JSON-LD. Das Gültigkeitsende ist bekannt; historischer Zustand und eigene Adresse bleiben erhalten. Der feste Stichtag dient hier zur statischen Erzeugung eines bereits abgeschlossenen Eintrags. Noch wechselnde Gültigkeit wird pro Anfrage oder durch Regenerierung am Grenzzeitpunkt behandelt.
Auch das Belegdokument tatsächlich bereitstellen
Speichern Sie den folgenden Code als app/policies/opening-hours/page.tsx. Die Seite liefert Öffnungszeiten, Schließtage und Gültigkeitsbeginn; sourceUrl der Detailseite verweist hierhin.
import type { Metadata } from 'next'
import { SITE_ORIGIN } from '../../../lib/site'
export const metadata: Metadata = {
title: 'Ursprüngliche Bibliotheksöffnungszeiten — Übungsmaterial',
alternates: { canonical: new URL('/policies/opening-hours', SITE_ORIGIN).href },
}
export default function Policy() {
return <main><article><h1>Ursprüngliche Bibliotheksöffnungszeiten — Übungsmaterial</h1>
<p>Öffnungshinweise der fiktiven Bibliothek ab 1. Oktober 2026.</p>
<p>Sonntags 10:00–17:00 Uhr. Am ersten Sonntag jedes Monats und an Feiertagen geschlossen.</p>
<p>Bekannt gegeben <time dateTime="2026-09-30">30. September 2026</time></p>
<a href="/guides/opening-hours">Zurück zur Nutzungsanleitung</a>
</article></main>
}
sourceUrl führt zu diesem Pfad. Öffnen Sie ihn und vergleichen Sie Zeiten, Ausnahmen und Gültigkeitsbeginn. Ein 200-Link zu einem anderen Jahr oder Standort ist kein passender Beleg. Auch die Quellseite bietet ihre eigene Adresse und einen Rückweg zur Anleitung.
Gestreamte Metadaten lesen
title, description und canonical der Details verwenden dieselben Daten wie der Inhalt. Der Titel nennt das Thema, die Zusammenfassung Zielgruppe und Informationen. Bei Next.js-Metadatenstreaming kann head nach Abschluss der Abfrage ergänzt werden. Vergleichen Sie Dokumentwerte erst nach Empfang der gesamten Antwort. Next.js-Metadaten
Absatzarrays, Markdown und Medien behandeln
guide.paragraphs ist ein Array von Absätzen. map verwandelt sie in p-Elemente; key identifiziert React-Listeneinträge. Gewöhnliche Zeichenfolgen in geschweiften Klammern werden als Text verarbeitet. sourceUrl enthält eine geprüfte Belegadresse; öffnen Sie sie zur Bestätigung desselben Materials.
Markdown beschreibt Überschriften, Listen und Links mit einfachen Symbolen. HTML- und Markdown-CMS verwenden Renderer zur Umwandlung gespeicherter Texte in HTML. Externe Eingaben erhalten Regeln für erlaubte Tags, Attribute und URLs gegen Skriptinjektion. Endloslisten benötigen eindeutige URLs und Links zu weiteren Artikeln. Prüfen Sie Entdeckungspfade für Material, das nur nach „Mehr laden“ erscheint.
Bilder erhalten zum Text passende Beschreibungen und Alternativtexte. Wichtige Zeit- und Gebührentabellen dürfen nicht ausschließlich Bilder sein; bieten Sie lesbaren Text oder Tabellen. Bildschirmgerechte Größen sowie Breite und Höhe reduzieren Übertragung und Layoutverschiebung. Videometadaten gehören nur auf Seiten mit tatsächlichem Video; prüfen Sie Wiedergabeadresse, Vorschaubild und Erreichbarkeit.
Strukturierte Daten aus dem Inhalt ableiten
„Kim Minsu, 24. Oktober“ allein klärt nicht unbedingt, ob die Person Autor oder Gegenstand ist und ob das Datum Veröffentlichung oder Ereignis meint. Strukturierte Daten benennen Beziehungen wie author, about und datePublished. Google nutzt sie zum Inhaltsverständnis und bei unterstützten Typen für erweiterte Suchanzeigen. Aufgabe strukturierter Daten
„Am ersten Sonntag geschlossen“ ist die antwortende Tatsache; publisher und dateModified in JSON-LD beschreiben Herausgeber und Änderungszeit. Aus derselben Vorlage erzeugt, zeigen sie gemeinsam, wer wann welchen Hinweis bereitstellte. WebPage allein erzeugt keine Schließungsbedingung; der Satz wird deshalb in p-Elementen ausgegeben.
Wählen Sie passende Typen, etwa Article für Artikel und WebPage für Nutzungshinweise. article-jsonld.mjs erzeugt sichtbaren Titel, Veröffentlichungs- und Änderungsdaten, Bild und JSON-LD aus einem Artikeldatensatz. Speichern Sie die Datei neben identity-time.mjs. node article-jsonld.mjs gibt JSON aus; vollständiges HTML steht unter /news/new-hours des Bildübungsservers.
import { pathToFileURL } from 'node:url';
import { describeDocument, formatContentDate } from './identity-time.mjs';
export const article = {
id: 'new-hours', path: '/news/new-hours', status: 'published', deletedAt: null,
title: 'Geänderte Sonntagsöffnungszeiten der Bibliothek',
summary: 'Ab Oktober 2026 sonntags 10–17 Uhr geöffnet. Am ersten Sonntag und an Feiertagen geschlossen.',
publishedAt: '2026-09-30T09:00:00+09:00',
publicFrom: '2026-09-30T09:00:00+09:00',
contentModifiedAt: '2026-10-01T09:00:00+09:00',
eventDate: null, validFrom: '2026-10-01T00:00:00+09:00', validUntil: null,
};
export const escapeHtml = value => String(value).replace(/[&<>"']/g,
c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
export function renderArticle(origin, image, now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z')) {
const identity = describeDocument(article, now, origin);
if (!identity) throw new Error('Article is not public');
// image erhält dieselbe öffentliche Bild-URL und tatsächliche Umwandlungsmaße.
const imageUrl = new URL(image.path, origin).href;
const structured = {
'@context': 'https://schema.org', '@type': 'Article', '@id': identity.id,
headline: article.title, mainEntityOfPage: identity.canonical,
datePublished: identity.datePublished, dateModified: identity.dateModified,
image: [imageUrl],
author: { '@type': 'Organization', name: 'Redaktion der fiktiven Bibliothek' },
publisher: { '@type': 'Organization', name: 'Fiktive Bibliothek' },
};
const h = escapeHtml;
const json = JSON.stringify(structured).replace(/</g, '\\u003c');
const html = `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${h(article.title)}</title><link rel="canonical" href="${h(identity.canonical)}">
<meta property="og:image" content="${h(imageUrl)}">
<script type="application/ld+json">${json}</script></head>
<body><article><h1>${h(article.title)}</h1>
<p>Veröffentlicht <time datetime="${h(identity.datePublished)}">${formatContentDate(identity.datePublished)}</time>
· Geändert <time datetime="${h(identity.dateModified)}">${formatContentDate(identity.dateModified)}</time></p>
<p>${h(article.summary)}</p><figure>
<img src="${h(imageUrl)}" width="${image.width}" height="${image.height}"
style="max-width:100%;height:auto" alt="Übungsbild für Bibliothekshinweise">
<figcaption>Beispiel zum Vergleich von Bildauslieferungswegen.</figcaption>
</figure><p>Redaktion der fiktiven Bibliothek</p></article></body></html>`;
return { structured, html };
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
console.log(JSON.stringify(renderArticle('https://example.com', {
path: '/media/photo?w=1280', width: 1280, height: 800,
}).structured, null, 2));
}
@context bezeichnet die Vokabularadresse, @type den Dokumenttyp, headline den Titel und mainEntityOfPage die repräsentierende Seite. datePublished ist die Veröffentlichungszeit, dateModified die Inhaltsänderung. Article steht für Artikel, NewsArticle für Nachrichten, SocialMediaPosting für soziale Beiträge, VideoObject für Video. Wählen Sie Typen und Eigenschaften entsprechend dem tatsächlichen Format.
JSON-LD wird als application/ld+json-Skript ausgegeben. Serialisieren Sie enthaltene Nutzereingaben sicher, damit keine schließende script-Zeichenfolge eingeschleust wird. Das Next.js-JSON-LD-Beispiel zeigt die Ersetzung des Zeichens < in der serialisierten Zeichenfolge durch einen Unicode-Escape. Prüfen Sie anschließend JSON-Parsing und Übereinstimmung mit dem Inhalt.
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(structured).replace(/</g, '\\u003c')
}}
/>
Prüfen Sie Typen und Eigenschaften sowie Titel, Autor und Daten gegen den Seitentext. Meldet das Suchwerkzeug Fehler, korrigieren Sie Wert und Datentyp der betroffenen Eigenschaft. Anforderungen an strukturierte Daten
JSON.stringify macht ein Objekt zur JSON-Zeichenfolge; replace ersetzt Zeichen. Der Unicode-Escape \u003c stellt in JSON das Zeichen < dar. dangerouslySetInnerHTML fügt Zeichenfolgen in React direkt als HTML ein. Übergeben Sie hier ausschließlich sicher serialisierte strukturierte Daten.
renderArticle prüft Öffentlichkeit und Daten mit describeDocument und erzeugt eine absolute Bildadresse für img, og:image und Article.image. escapeHtml behandelt Sonderzeichen in Attributen und Text. Die JSON-Ersetzung von < verhindert, dass ein script-Ende als HTML interpretiert wird. Der nachfolgende JSX-Block nutzt dieselbe Serialisierung in einer React-Seite.
Tatsächliche Bildbreite und -höhe reservieren in img Platz, bevor die Datei eintrifft. Veröffentlichung und Änderung haben eigene Labels; die Gültigkeit steht im Artikeltext. Damit werden Bild- und Datumsbeziehungen für die Suche angegeben und Belege der Änderung für Leser bereitgestellt. Googles Article-Anleitung empfiehlt ein zum Artikel passendes, crawlbares Hauptbild.
Für eine kleine Karte können 320px genügen, während ein Such-Hauptbild mehr Detailauflösung braucht. Die Übung zeigt die Beziehung mit derselben 1280px-Variante. Prüfen Sie in echten Artikeln Bildinhalt und Beschnitt und erstellen Sie bei Bedarf Formate wie 1:1, 4:3 und 16:9. Verwenden Sie nicht standardmäßig Szenen außerhalb des Textes oder Bilder anderer Artikel.