Ich hatte einen Blogpost eine Woche im Voraus terminiert. Die Post-Seite prüft das Datum und ruft notFound(), Sitemap und RSS filtern beide darauf — für mich war die Sache damit unsichtbar.
Dann habe ich den Build-Output nach einem Satz aus dem unveröffentlichten Post durchsucht und ihn auf /en/work gefunden.
Was tatsächlich passiert
next-intl schreibt den kompletten Message-Katalog in den RSC-Payload jeder Seite. Meine Blog-Inhalte liegen in messages/en.json und messages/de.json — also lieferte jede Seite den Volltext jedes Artikels aus, auch den des Posts, den es noch nicht geben sollte.
Die Routen-Sperre tut, was sie soll: 404. Nur hat sie nichts damit zu tun, ob der Text in der Antwort steht. Der kommt über den NextIntlClientProvider im Layout, nicht über die Seite.
Wer den Quelltext öffnet, liest alles. Jeder Crawler ebenfalls.
Der Fix
Die Messages in i18n/request.ts filtern, bevor sie bei getMessages() ankommen:
function withoutScheduledPosts(messages: Messages, now: Date): Messages {
const scheduled = posts
.filter((post) => new Date(`${post.publishDate}T00:00:00`) > now)
.map((post) => post.translationKey);
if (scheduled.length === 0) return messages;
const articles = { ...messages.blog?.articles };
for (const key of scheduled) delete articles[key];
return { ...messages, blog: { ...messages.blog, articles } };
}
Danach enthielten 0 von 44 gebauten HTML-Dateien den geplanten Post. An den veröffentlichten Inhalten hat sich nichts geändert.
Zwei Dinge, die der Filter nicht abdeckt
Statische Dateien unter public/. Ich pflege eine llms-full.txt, in der jeder Post mit URL und Zusammenfassung steht. Da kommt der Filter nicht hin, das ist eine flache Datei. Den Eintrag habe ich von Hand entfernt; er kommt am Release-Tag zurück.
Static Generation. Der Filter läuft zur Render-Zeit, bei SSG fällt die Entscheidung also beim Build. Ein geplanter Post braucht einen Deploy am Termin oder danach, sonst erscheint er nicht. Das galt schon vorher für die Publish-Sperre — der Filter macht es nicht schlimmer, aber man sollte es wissen, bevor man darauf wartet, dass ein Post von allein auftaucht.
Warum ich das auch ohne geplante Posts prüfen würde
Die allgemeine Form: Eine Routen-Sperre versteckt eine Seite, aber die Daten, die diese Seite rendert, kommen über einen Provider, der von der Sperre nichts weiß. Alles, was in deinem i18n-Katalog liegt, liegt damit auf jeder Seite, dauerhaft — Entwürfe, interne Texte, Feature-Flag-Strings, unveröffentlichte Produktnamen.
Die Prüfung ist ein Befehl gegen den Build-Output. Grep nach einem Satz, der nicht öffentlich sein darf, und sieh nach, wie viele Dateien zurückkommen.
Gefunden habe ich das beim Terminieren eines Posts über bewertete Agenten-Loops. Die Ironie eines Lecks im Terminierungsmechanismus ist mir nicht entgangen.
next-intl 4.13.4, Next 16.2.9, App Router.
Baust du an etwas Ähnlichem?
Ich schreibe hier über Setups, die ich selbst benutze. Wenn du an etwas Vergleichbarem arbeitest, würde mich interessieren, wie dein Workflow aussieht.