Änderungen prüfen, bereitstellen und wiederherstellen
Dieselben URLs vor und nach Änderungen vergleichen und Produktionsbuild, Inhalt, Status und Cache prüfen. Die über die Produktionsdomain ausgelieferte Version feststellen und je nach Fehler Code, Text oder Daten wiederherstellen.
Vor und nach der Änderung dieselben URLs verwenden
Ein Build prüft, ob aus dem Code ein bereitstellbares Ergebnis entsteht. Ob Text, URLs und Daten für die Suche stimmen, zeigen erst tatsächliche Antworten. Ein erfolgreicher Build verbessert die Auslieferung nicht, wenn in Produktion noindex bleibt oder das CDN alte Inhalte sendet. Der Vergleich derselben URLs zeigt, welche Antworten der geänderte Code verändert hat.
Die URL-Liste aus der Diagnose als Prüfeingabe verwenden: öffentliche Detailseiten, erste und weitere Listenseiten, Bilder und Anhänge, alte und fehlende URLs sowie Entwürfe und zurückgezogene Inhalte. Vorher Statuscode, endgültige URL, robots, canonical, Titel und Kernsätze speichern. Nachher dieselben Werte erfassen, damit Unterschiede sichtbar werden.
Ziel-URL, Sollwert, Istwert, Prüfzeit und Bereitstellungsversion protokollieren. Nicht nur das Vorhandensein in der öffentlichen Liste prüfen, sondern die Detailseite öffnen und mit dem Original vergleichen. Wurden nur Metadaten verändert, auch den Erhalt von Text, Fotos und Links kontrollieren.
Codeprüfungen passend zur Änderung auswählen
Bei Änderungen an Veröffentlichungsprüfung oder URL-Erzeugung Eingaben mit unterschiedlichen Ergebnissen betrachten: öffentlich, Entwurf, noch nicht fällig, zurückgezogen und fehlend. Bestehende Tests im betreffenden Bereich ausführen. Typprüfung und Produktionsbuild prüfen die Bereitstellbarkeit. Die folgenden Befehle setzen typecheck- und build-Skripte in package.json voraus. Bei anderen Namen die Befehle des jeweiligen Repositories verwenden.
npm run typecheck
npm run build
Auch nach bestandener Typprüfung und Build Seiten im tatsächlichen Produktionsmodus öffnen. Produktionsdomain und öffentliche API-Adressen zwischen Build- und Laufzeitumgebung vergleichen. Umgebungsspezifische Werte prüfen, damit noindex und Authentifizierung der Testumgebung nicht unverändert in Produktion gelangen.
Fehlerhafte Veröffentlichungszustände, Fehler bei regulären Artikeln oder fehlende Texte und Links zuerst korrigieren, dann den betroffenen Bereich erneut prüfen. Indexierung und externe Zitate lassen sich in dieser Phase nicht lokal feststellen; sie gehören in die Beobachtung nach der Bereitstellung.
Rendering anhand von Build und tatsächlichen Antworten prüfen
Zuerst build-Befehl in package.json, Next.js-Version, next.config sowie Datenabfragen und Cache-Einstellungen lesen. Den Produktionsbuild ausführen und Seiten aus diesem Ergebnis öffnen. Build-Ausgabe und Bereitstellung zeigen statische, anfragebasierte und revalidierte Routen. Bei output: export die erzeugten HTML-Dateien sowie direkte Hosting-URLs und 404-Behandlung prüfen.
Ein Parser liest Dokumentzeichenfolgen als Struktur. Öffentliche Detail-URLs mit curl abrufen und article, h1, Absätze sowie a href mit einem HTML-Parser extrahieren. Prüfen, ob Kernsätze des Originals in sichtbaren Inhaltselementen stehen. Text ausschließlich in serialisierten Next.js-Daten oder script-Zeichenfolgen ist kein HTML-Seiteninhalt. Streaming-Antworten vollständig empfangen und fehlenden Inhalt nicht allein aus dem ersten Ladefragment ableiten.
Im Browser direkten URL-Aufruf und Navigation über Listenlinks prüfen. Bleiben Kerninhalt und Quellen vor und nach der Hydration erhalten? Werden zusätzliche Daten erst beim Klicken oder Scrollen geladen? Deaktiviertes JavaScript ist eine ergänzende Abhängigkeitsprüfung. Skriptgestützte Streaming-Platzierung berücksichtigen und Originalantwort, fertiges DOM und Rendering des Suchwerkzeugs gemeinsam lesen.
Bei Metadaten gewöhnliche Anfragen und Antwortbedingungen des Zielcrawlers unterscheiden. Next.js kann Metadaten streamen oder bei als reine HTML-Leser eingestuften Bots darauf warten. Mit unterschiedlichen User-Agent-Werten in curl die Antwortzweige vergleichen. Tatsächliche Bot-Besuche und Abrufresultate aus Serverlogs und Suchwerkzeugen ermitteln. Die Metadata-API-Anleitung zu Streaming und Bots mit der installierten Version abgleichen.
Auch nach notFound() für fehlende Artikel den HTTP-Status in Produktion direkt lesen. Nach Streaming-Beginn kann eine Fehlerseite mit 200 eintreffen. Bei erforderlichem echtem 404 Existenz und Veröffentlichungsstatus vor dem Senden der Header prüfen können; außerdem noindex kontrollieren. Die Next.js-not-found-Regeln erklären Statusunterschiede zwischen Streaming und nicht gestreamten Antworten.
Nach Veröffentlichung, Änderung und Rücknahme die aktuellen Werte von Detailseite, Liste, Sitemap und Metadaten erneut lesen. Statische oder gecachte Seiten mit gefülltem Cache ändern und erste Anfrage mit einer Anfrage nach abgeschlossener Aktualisierung vergleichen. Öffentliche Version, Sollinhalt, Istinhalt, Antwortzeit und Revalidierungszeit festhalten. Bei verbliebenem Alttext Daten-Cache, Seiten-Cache und CDN als Quelle eingrenzen.
Antwort, Metadaten und Inhalt prüfen
Die folgenden Befehle in der Prüfumgebung ausführen. Getrennte Dateien für HTML und Header machen auch X-Robots-Tag vergleichbar. Adresse und Dateinamen für jede Ziel-URL ändern.
CHECK_URL='https://example.com/guides/opening-hours'
curl -sS -D after.headers -o after.html "$CHECK_URL"
rg -n -i 'HTTP/|location:|x-robots-tag:|content-type:' after.headers
rg -n -i 'canonical|noindex|<title|application/ld\+json' after.html
Öffentliche kanonische URLs müssen erfolgreichen Status, vorgesehenes canonical und nicht blockierende robots-Anweisungen zeigen. Direkte Antwort und Quellenlinks im tatsächlichen Inhalt suchen. Zeigt eine 200-Antwort Anmeldung, Bot-Prüfung oder „nicht gefunden“, Zugriffseinstellungen oder Fehlerbehandlung korrigieren. Beim Aufheben des Suchausschlusses darf noindex weder in meta noch in Headern verbleiben.
Streaming sendet Antworten in Teilen, sobald diese bereit sind. Tagpositionen können je nach Next.js-Metadatenstreaming und Serverrendering variieren. Vollständige Antwort und Browser-DOM gemeinsam lesen. Unterschiede zwischen normalen und Bot-User-Agents anhand von Code und CDN-Regeln untersuchen. Googlebot in curl zu nennen reproduziert einen Antwortzweig; tatsächliches Google-Crawling belegen URL-Prüfung und Serveraufzeichnungen.
Erwartete Beispielantworten direkt vergleichen
Die folgenden Pfade vom Übungsserver im Produktionsmodus anfordern. Öffentliche Artikel sollen 200 und Öffnungszeitinhalt liefern, Entwürfe und fehlende Artikel 404 mit Suchausschluss, die Archivseite erzeugten Inhalt. Die Sitemap enthält öffentliche Detail- und Archiv-URLs, keine Entwurfsadressen.
curl -sS -D guide.headers -o guide.html http://localhost:3027/guides/opening-hours
curl -sS -D draft.headers -o draft.html http://localhost:3027/guides/draft-guide
curl -sS -D missing.headers -o missing.html http://localhost:3027/guides/missing
curl -sS http://localhost:3027/archive/opening-2025
curl -sS http://localhost:3027/sitemap.xml
Mit dem HTML-Parser article-Inhalt und data-revision lesen und prüfen, ob title, canonical und JSON-LD dieselbe Fassung verwenden. Serialisierte script-Zeichenfolgen von wirklichen Inhaltselementen trennen. Lokale und Produktionsdomain-Ergebnisse separat dokumentieren.
Auch /archive/missing und /policies/opening-hours anfordern. Ersteres erwartet 404, noindex und kein canonical; Letzteres 200 mit Öffnungszeitinformation. Neben Statuscodes auch Ausnahmen in Serverlogs prüfen. Die Archivdatei liegt unter app/archive/opening-2025/page.tsx.
Inhalts-, URL- und Datumsabweichungen mit einem HTML-Parser finden
check-pages.mjs und validate-dates.mjs unten zusammen mit identity-time.mjs im Übungsordner speichern. Den Parser mit npm install --save-dev cheerio@1.2.0 installieren. Den Produktionsserver der Übung starten und node check-pages.mjs http://localhost:3027 ausführen.
// Installation: npm install --save-dev cheerio@1.2.0
// Ausführen: node check-pages.mjs http://localhost:3027
import assert from 'node:assert/strict';
import { load } from 'cheerio';
import { validateDates } from './validate-dates.mjs';
const base = new URL(process.argv[2] || 'http://localhost:3027');
const origin = 'https://example.com'; // Gleicher Wert wie SITE_ORIGIN im Beispiel
const results = [];
for (const [path, status] of [
['/guides/opening-hours', 200], ['/guides/draft-guide', 404],
['/guides/missing', 404], ['/archive/opening-2025', 200],
['/archive/missing', 404], ['/policies/opening-hours', 200],
]) {
const response = await fetch(new URL(path, base), {
redirect: 'manual', signal: AbortSignal.timeout(10000),
});
assert.equal(response.status, status, path + ': status');
const $ = load(await response.text());
const canonicals = $('link[rel="canonical"]');
const robots = $('meta[name="robots"]').map((_, e) => $(e).attr('content')).get().join(',')
+ ',' + (response.headers.get('x-robots-tag') || '');
if (status === 404) {
assert.match(robots, /noindex/i, path + ': noindex');
assert.equal(canonicals.length, 0, path + ': inherited canonical');
assert.equal($('article[data-revision]').length, 0, path + ': leaked article');
} else {
assert.equal(canonicals.length, 1, path + ': canonical count');
assert.equal(canonicals.attr('href'), origin + path, path + ': canonical');
assert.doesNotMatch(robots, /noindex/i, path + ': index blocked');
}
if (path === '/guides/opening-hours') {
const data = JSON.parse($('script[type="application/ld+json"]').text());
assert.equal(data.url, origin + path);
assert.equal(data.name, $('article h1').text());
validateDates($, data);
const body = $('article').clone(); body.find('script').remove();
assert.match(body.text(), /ersten Sonntag jedes Monats und an Feiertagen/);
assert.equal($('article a').attr('href'), '/policies/opening-hours');
}
results.push({ path, status, passed: true });
}
const xml = await fetch(new URL('/sitemap.xml', base));
assert.equal(xml.status, 200);
const $xml = load(await xml.text(), { xmlMode: true });
const urls = $xml('loc').map((_, e) => $xml(e).text()).get();
assert(urls.includes(origin + '/guides/opening-hours'));
assert(urls.includes(origin + '/archive/opening-2025'));
assert(urls.includes(origin + '/policies/opening-hours'));
assert(!urls.some(url => /draft-guide|missing/.test(url)));
const moved = await fetch(new URL('/guides/library-hours', base), { redirect: 'manual' });
assert.equal(moved.status, 308);
assert.equal(new URL(moved.headers.get('location'), base).pathname, '/guides/opening-hours');
console.log(JSON.stringify({ pages: results, sitemap: urls }, null, 2));
validateDates unterscheidet Veröffentlichungs- und Änderungszeilen über data-date und gleicht Beschriftung, datetime und sichtbares Datum mit JSON-LD ab. Die bloße Existenz eines Datums irgendwo erkennt weder vertauschte Datumsrollen noch ein nur sichtbar auf 1999 geändertes Jahr. Abschließend werden auch die 308-Weiterleitung des alten Slugs und die Sitemap geprüft.
Das Ergebnis zeigt sechs Seitenstatus und die Sitemap-Liste. Es erscheint erst, wenn auch die 308-Prüfung des alten Slugs bestanden ist. datePublished: label bezeichnet eine falsche Beschriftung, dateModified: visible text ein abweichendes sichtbares Datum, inherited canonical eine auf der Fehlerseite geerbte kanonische URL. Das tatsächliche HTML des Fehlerfalls speichern und das betreffende Ausgabefeld korrigieren.
import assert from 'node:assert/strict';
import { formatContentDate } from './identity-time.mjs';
export function validateDates($, data) {
for (const [key, label] of [['datePublished', 'Veröffentlicht'], ['dateModified', 'Geändert']]) {
const row = $(`article [data-date="${key}"]`);
const expected = data[key];
assert.equal(row.length, expected === undefined ? 0 : 1, key + ': row count');
if (expected === undefined) continue;
assert.equal(row.find('[data-label]').text().trim(), label, key + ': label');
assert.equal(row.find('time').length, 1, key + ': time count');
assert.equal(row.find('time').attr('datetime'), expected, key + ': datetime');
assert.equal(row.find('time').text().trim(), formatContentDate(expected), key + ': visible text');
}
}
formatContentDate verwendet auf der Seite und in der Prüfung dieselbe Anzeigeregel. Regressionstests vertauschen Beschriftungen, datetime-Werte oder nur das sichtbare Jahr und prüfen, dass jeder Fall fehlschlägt. Gültige und ungültige Eingaben gemeinsam prüfen, damit auch die Prüfung selbst Fehler nicht übersieht.
Die nächste Datei als validate-dates.test.mjs speichern. Im Übungsordner mit installiertem Cheerio führt node --test validate-dates.test.mjs eine gültige Eingabe und drei veränderte Fehlerfälle aus.
import test from 'node:test';
import assert from 'node:assert/strict';
import { load } from 'cheerio';
import { validateDates } from './validate-dates.mjs';
const data = { datePublished: '2026-10-01', dateModified: '2026-10-23T09:00:00+09:00' };
const html = `<article><p data-date="datePublished"><span data-label>Veröffentlicht</span><time datetime="2026-10-01">1.10.2026</time></p><p data-date="dateModified"><span data-label>Geändert</span><time datetime="2026-10-23T09:00:00+09:00">23.10.2026</time></p></article>`;
test('Korrekte Labels, Maschinendaten und sichtbare Daten bestehen', () => validateDates(load(html), data));
test('Vertauschte Labels, Maschinenwerte und falsche sichtbare Jahre schlagen jeweils fehl', () => {
const labelSwap = html.replace('Veröffentlicht', 'TEMP').replace('Geändert', 'Veröffentlicht').replace('TEMP', 'Geändert');
const valueSwap = html.replace('datetime="2026-10-01"', 'datetime="TEMP"')
.replace('datetime="2026-10-23T09:00:00+09:00"', 'datetime="2026-10-01"')
.replace('datetime="TEMP"', 'datetime="2026-10-23T09:00:00+09:00"');
for (const bad of [labelSwap, valueSwap, html.replace('1.10.2026', '1.1.1999')])
assert.throws(() => validateDates(load(bad), data));
});
Sichtbare Datumsbeschriftungen werden geprüft, weil dieselben Zahlen unterschiedliche Bedeutung haben können. Ein Geltungsbeginn an der Stelle von „Veröffentlicht“ kann künftige Öffnungszeiten wie aktuelle Hinweise aussehen lassen. validateDates prüft Veröffentlichungs- und Änderungslabels samt Werten. Ob Geltungszeitraum und Betriebsfakten übereinstimmen, wird durch Vergleich von Text und Belegen geprüft.
Inhalte und Verbindungen im Browser direkt nutzen
Die Liste ohne Anmeldung öffnen und ersten, mittleren und letzten Artikel anklicken. Folgeseiten und verwandte Artikel aufrufen; nach Zurück und Neuladen den Inhaltserhalt prüfen. Fotos und Anhänge wirklich öffnen, Videos abspielen. Quellenlinks dürfen nicht zu anderen Dokumenten mit nur ähnlichem Titel führen.
Auf breiten und schmalen Ansichten Umbrüche von Titeln, langen URLs und Code lesen. Bei vertikalen Tabellen prüfen, ob jeder Wert seine Spaltenüberschrift behält. Themes wechseln und Kontrast vergleichen; Sprachen wechseln und erhaltene Absätze sowie Links kontrollieren.
Network in den Browserwerkzeugen zeigt Anfragen, Antworten und Übertragungszeiten; Console zeigt Ausführungsmeldungen und Fehler. Fehlgeschlagene Dokument-, Bild- und Skriptantworten prüfen und ihre funktionalen Auswirkungen verfolgen. Existiert Inhalt nur im Browser, DOM und Datenanfragen vor und nach JavaScript vergleichen. Googles Verarbeitung nach Bereitstellung erneut über die URL-Prüfung kontrollieren.
Sitemap, Links und strukturierte Daten abgleichen
Die Sitemap-URL-Menge mit den erwarteten öffentlichen URLs vergleichen. Dubletten, Auslassungen, zurückgezogene und weiterleitende URLs getrennt erfassen. Bei Sitemap-Indizes auch Unterdateien lesen. Beim Abruf aller Statuscodes Last berücksichtigen, Parallelität und Rate begrenzen sowie Fehler-URLs und Ursachen speichern.
canonical der Detailseite, Sitemap-URL und interne Links auf dieselbe Adresse prüfen. Sind alle Änderungsdaten mit der Bereitstellungszeit überschrieben, den Erzeugungscode korrigieren. Bei hreflang erfolgreiche Antworten und Rückverweise auf den anderen Sprachseiten prüfen.
Alle application/ld+json-Blöcke der Seite als JSON parsen. Zuerst Syntaxfehler beheben, dann Titel, Daten, Autoren und URLs mit sichtbarem Inhalt vergleichen. Ein Rich Result zeigt zusätzliche Informationen neben dem gewöhnlichen Suchlink. Von Google unterstützte Typen zusätzlich mit Rich Results Test prüfen. Bei Warnungen fehlende Pflicht- und empfohlene Felder unterscheiden und mit der Typdokumentation vergleichen.
Veröffentlichung, Änderung, Rücknahme und Wiederherstellung über dieselben Wege prüfen
Nach Änderungen an Inhaltsfunktionen den Lebenszyklus eines Artikels prüfen. Entwürfe fehlen auf öffentlichen Ansichten; nach Veröffentlichung stimmen Liste, Detail, Sitemap und Teilen-Informationen überein. Nach Änderung von Titel, Daten und Belegen auf alte Cache-Werte prüfen. Nach Rücknahme dürfen öffentliche Links, eigene Suche und eigener Chatbot den Artikel nicht weiter nutzen.
Zur Wiederherstellungsprüfung ein Testdokument veröffentlichen, ändern, zurückziehen und eine frühere revision erneut veröffentlichen. Wiederhergestellten Text, Quellen und Bilder lesen und den Erhalt des aktuellen Arbeitsentwurfs prüfen. Erstveröffentlichungsdatum bewahren; den Zeitpunkt einer inhaltlichen Änderung durch Wiederherstellung als Änderungsdatum erfassen.
Bei neuen Inhaltstypen gemeinsame Veröffentlichungsbedingungen von Liste, Detail und Sitemap prüfen. Fehlt ein sichtbarer Typ in Sitemap, verwandten Artikeln oder Überwachung, die Registrierung korrigieren. In Listen aus zwei Quellen einen Abfragefehler nicht stillschweigend als leere Daten behandeln. Die Oberfläche kann Teilausfälle anzeigen; Sitemap und Überwachung, die vollständige Listen feststellen, müssen unvollständige Ergebnisse gesondert speichern.
Überarbeitete Haupt- und Archivseiten mit Originaltext, Fotos und Links vergleichen. Nicht automatisiert erfasste Bereiche nach Klicks, Videos und browserspezifische Darstellung im Prüfumfang festhalten. Von Listen zu Details, verwandten Artikeln und Quellen navigieren und Inhalte sowie Zieladressen vergleichen.
Bereitstellungsversion und Produktionsdomain prüfen
Vor der Bereitstellung geänderte Dateien und Inhalte prüfen und die Kennung der letzten funktionierenden Version festhalten. Bei gemeinsamen Code- und Textänderungen die bereitgestellte Kombination dokumentieren. Speicherstrukturen so ändern, dass vorhandene Daten lesbar bleiben. Neue Felder, Datenauffüllung, Leseumstellung und Entfernung alter Felder trennen, um Konflikte zwischen Zwischenversionen zu verringern.
Build-Abschluss und Commit- oder Release-ID im Bereitstellungswerkzeug prüfen. Anschließend sicherstellen, dass die tatsächliche Produktionsdomain auf diese Version zeigt. Vorschau und Produktion getrennt anfragen. Betroffene öffentliche Pfade erneut abrufen und Header, Inhalt, canonical und Sitemap abgleichen.
Bei CMS- und CDN-Caches Revalidierung und Invalidierung entlang der Inhaltsaktualisierung prüfen. Ist der Artikel neu, Liste, Sitemap oder Vorschaukarte aber alt, die jeweils gelesenen Daten und Caches ermitteln. Nach Änderung oder Rücknahme müssen alle öffentlichen Wege denselben Zustand zeigen.
DNS, TLS, Caches und vorgelagerte Sperren im Produktionsweg prüfen
Mit dig die DNS-Einträge A, AAAA und CNAME abfragen. dig ist ein Werkzeug für DNS-Abfragen. Tatsächliche Antworten mit Hostingvorgaben vergleichen und konfigurierte IPv4- und IPv6-Wege getrennt prüfen. example.com und die folgenden Pfade durch die öffentlichen Prüfziele ersetzen.
dig +noall +answer example.com A
dig +noall +answer example.com AAAA
dig +noall +answer www.example.com CNAME
CHECK_URL='https://example.com/guides/opening-hours'
curl -sS --max-time 30 -D infra.headers -o infra.html \
-w 'status=%{http_code} dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} first_byte=%{time_starttransfer} total=%{time_total}\n' \
"$CHECK_URL"
rg -n -i 'HTTP/|location:|cache-control:|age:|etag:|vary:|x-robots-tag:|content-type:' infra.headers
curl sendet Anfragen an URLs. -D speichert Antwortheader, -o den Inhalt und -w gibt gewählte Messwerte aus. --max-time 30 wartet höchstens 30 Sekunden auf die gesamte Anfrage. Zeiten sind kumulierte Sekunden seit Anfragebeginn. time_appconnect bezeichnet den abgeschlossenen TLS-Aufbau, time_starttransfer das erste Antwortbyte. Beide enthalten vorherige Stufen und sind nicht allein Serververarbeitungszeiten.
Die standardmäßige Zertifikatsprüfung von curl verwenden. Bei Fehlern Domainname, Ablaufdatum, Zertifikatskette und Verbindungsweg prüfen. Mit -k übersprungene Prüfung kann die Zertifikatsgültigkeit nicht belegen. Auch HTTP anfragen und die Weiterleitung zur kanonischen HTTPS-Adresse kontrollieren.
Header und Inhalt der ersten und wiederholten Anfrage derselben URL speichern. Age zeigt das Alter der gespeicherten Antwort; auch plattformspezifische Cache-Header lesen. Selbst bei Cache-Treffern mit der neuesten Veröffentlichung vergleichen. Nach Änderung oder Rücknahme des Prüfmaterials kontrollieren, wann Detailseite, Liste, Sitemap und Bilder aktualisiert werden.
Zeitpunkt und Pfad der Prüfanfrage in WAF- und CDN-Logs finden und mit Ursprungslogs verbinden. Bei Bot-Prüfseite, 403 oder 429 statt öffentlichem Artikel die zutreffende Regel prüfen. Kerninhalte für Menschen und Crawler verwenden dasselbe öffentliche Material. Auch im tatsächlichen Abruf des Suchwerkzeugs den Inhalt prüfen.
Je nach Fehlertyp zurücksetzen
Werden öffentliche Details nach Bereitstellung zu 5xx oder private Inhalte sichtbar, zuerst die vorherige funktionierende Bereitstellung wiederherstellen oder den betroffenen Pfad sperren. Bei korrektem Text und nur falschen Metadaten kann gezielt die bestätigte fehlerhafte Einstellung zurückgenommen werden. Danach ursprüngliche Fehler-URL und benachbarte Zustände erneut prüfen.
Ein Code-Rollback setzt CMS-Texte und Datenbankänderungen nicht automatisch zurück. Falsch veröffentlichte Texte aus der CMS-Historie wiederherstellen. Bei Strukturänderungen zuerst prüfen, ob alter Code die aktuellen Daten lesen kann. Ein vollständiger Datenbank-Restore als gewöhnliche Erstmaßnahme kann spätere korrekte Eingaben verlieren.
Symptome, betroffene URLs, Bereitstellungs-ID, Wiederherstellungszeit und anschließende Beobachtungen dokumentieren. Nach Normalisierung der Produktionsantworten getrennt verfolgen, wie lange alte Inhalte in Suche und KI-Antworten verbleiben. Erneutes Crawling folgt einem anderen Zeitpunkt als die Codebereitstellung.
Ändert sich externe Suche nach Wiederherstellung nicht sofort, zuerst den letzten Crawl-Zeitpunkt prüfen. Solange die Fehlerantwort im Suchsystem bleibt, kann es trotz gesundem aktuellem Server alte Daten zeigen. Wiederhergestellte Antwort mit späteren Abrufen verbinden, um weiteren Codebedarf von der Kontrolle externer Aktualisierung zu unterscheiden.
Fehler bei Bildern, Artikeln und Feeds durch Anfragen reproduzieren
Bei Bildoptimierung Dateigröße, Zugriff nach Rücknahme und Kandidatenbreiten gemeinsam prüfen. Als Eingaben dienen großes Original, kleines 200px-Original, Rücknahme während der Transformation und gleichzeitige Anfragen derselben Breite. media.test.mjs unten im Bildübungsordner speichern und mit node --test media.test.mjs ausführen. Der Server startet auf einem zufälligen lokalen Port und schließt danach.
import test from 'node:test';
import assert from 'node:assert/strict';
import sharp from 'sharp';
import { createMediaStore } from './media-store.mjs';
import { createDemoServer, sampleImage } from './media-demo.mjs';
import { article, renderArticle } from './article-jsonld.mjs';
import { renderFeed, escapeXml } from './feed.mjs';
const now = '2026-10-24T09:00:00+09:00';
test('Gleichzeitige Anfragen derselben Breite transformieren einmal und liefern die tatsächliche Pixelbreite', async () => {
const store = await createMediaStore(await sampleImage());
const [a, b] = await Promise.all([store.read(640), store.read(640)]);
assert.deepEqual(a.body, b.body); assert.equal(store.stats().transforms, 1);
assert.equal((await sharp(a.body).metadata()).width, 640);
assert.equal(a.headers['Cache-Control'], 'private, no-store, max-age=0');
assert.equal((await store.read(999)).status, 400);
});
test('Rücknahme während der Transformation liefert auch wartenden Anfragen 404 und sperrt Originale und Derivate', async () => {
const store = await createMediaStore(await sampleImage());
const pending = store.read(640);
store.setPublished(false);
assert.equal((await pending).status, 404);
for (const width of [null, 320, 640, 1280]) assert.equal((await store.read(width)).status, 404);
store.setPublished(true);
assert.equal((await store.read(640)).status, 200);
assert.equal(store.stats().transforms, 1);
});
test('Kleine Originale werden nicht vergrößert und srcset enthält keine doppelten Breiten', async () => {
const source = await sharp({ create: { width: 200, height: 100, channels: 3, background: 'red' } }).jpeg().toBuffer();
const store = await createMediaStore(source);
assert.equal((await store.read(1280)).width, 200);
const server = createDemoServer(store);
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
try {
const base = `http://127.0.0.1:${server.address().port}`;
const html = await (await fetch(base)).text();
assert.equal((html.match(/ 200w/g) ?? []).length, 1);
assert(!html.includes('1280w'));
await fetch(base + '/media/photo?w=640');
store.setPublished(false);
for (const path of ['/media/photo', '/media/photo?w=640', '/news/new-hours', '/'])
assert.equal((await fetch(base + path)).status, 404);
const feed = await (await fetch(base + '/rss.xml')).text();
assert(!feed.includes('<item>'));
} finally { await new Promise(resolve => server.close(resolve)); }
});
test('Sichtbares Bild, Vorschaubild, Article.image und Daten stammen aus derselben Quelle', () => {
const { html, structured } = renderArticle('https://example.com', { path: '/media/photo?w=1280', width: 1280, height: 800 }, now);
assert.equal(structured.image[0], 'https://example.com/media/photo?w=1280');
assert(html.includes(`src="${structured.image[0]}"`));
assert(html.includes(`content="${structured.image[0]}"`));
assert(html.includes(`datetime="${structured.datePublished}"`));
assert(html.includes(`datetime="${structured.dateModified}"`));
assert.throws(() => renderArticle('https://example.com', { path: '/photo' }, '2026-09-01T00:00:00Z'));
});
test('Der Feed schließt Entwürfe, Rücknahmen und geplante Beiträge aus und bewahrt ID und Erstveröffentlichung nach Änderungen und Umzügen', () => {
const changed = { ...article, path: '/news/revised-hours', summary: 'A & B < C', contentModifiedAt: now };
const xml = renderFeed([changed,
{ ...article, id: 'draft', status: 'draft' },
{ ...article, id: 'withdrawn', status: 'withdrawn' },
{ ...article, id: 'future', publicFrom: '2027-01-01T00:00:00Z' }], 'https://example.com', now);
assert.equal((xml.match(/<item>/g) ?? []).length, 1);
assert(xml.includes('A &amp; B &lt; C'));
assert(xml.includes('/#document-new-hours</guid>'));
assert(xml.includes('/news/revised-hours</link>'));
assert(xml.includes('Wed, 30 Sep 2026 00:00:00 GMT'));
assert(!renderFeed([{ ...article, status: 'withdrawn' }], 'https://example.com', now).includes('<item>'));
});
test('Der Feed bewahrt Datumspräzision und weist verbotene XML-Zeichen sowie umgekehrte Daten zurück', () => {
const xml = renderFeed([{ ...article, publishedAt: '2026-09-30' }], 'https://example.com', now);
assert(!xml.includes('<pubDate>'));
assert.throws(() => escapeXml('bad\u0000'));
assert.throws(() => escapeXml('bad\uD800'));
assert.throws(() => renderFeed([{ ...article, contentModifiedAt: '2026-09-01' }], 'https://example.com', now));
});
Der erste Test prüft eine einzige Transformation bei zwei gleichzeitigen Abrufen. Der zweite zieht während der Transformation zurück und prüft die wartende sowie spätere Original- und Derivatanfragen. Der dritte verhindert eine 1280w-Kennzeichnung am 200px-Original und prüft Rücknahme über HTTP. Der Artikeltest gleicht Datums- und Bildeingaben in HTML und JSON-LD ab. Feedtests prüfen Ausschluss von Entwürfen, geplanten und zurückgezogenen Beiträgen, stabile IDs, Datumspräzision und XML-Sonderzeichen.
Im Browser zusätzlich Bilddekodierung und fehlerfreies XML-Parsen des RSS prüfen. Den Viersekunden-Videolink anklicken, in ein neues Fenster kopieren und denselben Hinweis kontrollieren. Nach Änderung der Bildauslieferung müssen Text und Videosprünge weiter funktionieren, damit Leser die Belege erreichen.
Nach Erzeugung der Videodatei den folgenden Code als video.test.mjs speichern und node --test video.test.mjs ausführen. Bereiche am Dateianfang, am Ende und bis zum Ende anfordern und empfangene Bytes mit dem Original vergleichen. Bereiche außerhalb der Datei und umgekehrte Bereiche müssen 416 ergeben.
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { createDemoServer } from './media-demo.mjs';
// Nach Erzeugung von talk.mp4 mit node make-video-demo.mjs ausführen.
test('Video-Byte-Ranges liefern den angeforderten Bereich und weisen Anfragen außerhalb der Datei zurück', async () => {
const file = await readFile(new URL('./talk.mp4', import.meta.url));
const server = createDemoServer(null);
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
try {
const url = `http://127.0.0.1:${server.address().port}/talk.mp4`;
for (const [range, start, end] of [
['bytes=0-99', 0, 99], ['bytes=-100', file.length - 100, file.length - 1],
[`bytes=${file.length - 100}-`, file.length - 100, file.length - 1],
]) {
const response = await fetch(url, { headers: { Range: range } });
assert.equal(response.status, 206);
assert.equal(response.headers.get('content-range'), `bytes ${start}-${end}/${file.length}`);
assert.deepEqual(Buffer.from(await response.arrayBuffer()), file.subarray(start, end + 1));
}
for (const range of ['bytes=999999999-', 'bytes=20-10', 'bytes=-0', 'bytes=-'])
assert.equal((await fetch(url, { headers: { Range: range } })).status, 416);
} finally { await new Promise(resolve => server.close(resolve)); }
});