Separate private previews from public blog pages, guard metadata and serialized data, and invalidate affected routes after publishing changes.

A published blog post and an editor's preview can use the same database row, but they should not use the same public cache. If a draft is read through a trusted server helper and then serialized into a shared page, hiding the editor controls will not make the content private.
In a Next.js App Router blog, decide which requests may see unpublished data before choosing a cache duration. This guide uses BTST v3 Blog as a concrete example; the same boundary matters in a custom database-backed publishing system.
BTST's low-level server getters are useful for trusted server code. Calling a getter is not a substitute for an application's authorization policy. A public route must constrain its result before rendering, generating metadata, or dehydrating query data.
For a route that uses the low-level Blog getter, an application-owned helper can enforce the public condition:
import { cache } from "react";
import { notFound } from "next/navigation";
import { getPostBySlug } from "@btst/stack/plugins/blog/api";
import { myStack } from "@/lib/stack";
export const getPublishedPost = cache(async (slug: string) => {
const post = await getPostBySlug(myStack.adapter, slug);
if (!post?.published) notFound();
return post;
});
Here myStack is your existing configured backend stack. This helper is a public-read boundary, not a complete page implementation. Call it from both the page and its metadata function, before any raw prefetch that could expose the row. React's cache() can deduplicate the helper within the server render; it does not turn the result into a permanent public cache.
An editor preview needs a different request-aware path with an authenticated identity and a permission check. Do not make the public helper return drafts when an optional browser flag is present. A noindex directive is also not access control: it cannot prevent someone from reading a response that the server already returned.
Inspect more than the visible article component. A response can omit the body yet still expose an unpublished title, excerpt, Open Graph description, or dehydrated query record.
For a private draft, request the public slug without cookies and search the entire response for a distinctive private sentence and title. Also test the public list, tag pages, sitemap, and any JSON API used by the browser. Public list queries should request published content; editor list queries must enforce their own policy.
Next.js notFound() renders the route's not-found UI and adds a noindex signal. Check the actual status and response for your deployed rendering setup rather than assuming a client redirect provides the same result.
Publishing changes at least two public surfaces: the article and the article list. Editing a title can also change metadata, tag views, and cards on other pages. Unpublishing removes permission for new anonymous reads, so stale public copies deserve particular attention.
Inside a Next.js server mutation lifecycle, invalidate the affected paths after a successful write. This excerpt shows the public-list and article paths for an app mounted under /p:
import { revalidatePath } from "next/cache";
export function invalidatePublishedPost(slug: string) {
revalidatePath("/p/blog");
revalidatePath(`/p/blog/${slug}`);
}
Use it in the server lifecycle of an authorized mutation, not in a browser component or standalone shell script. If the slug changes, retain and invalidate the old path as well. If tag pages or a custom feed are cached separately, invalidate those affected surfaces too. A removed or unpublished post still needs its former public detail path invalidated.
The Next.js revalidation reference explains the different behavior of calls from Server Functions and Route Handlers. A framework cache invalidation also cannot erase copies visitors already downloaded. For sensitive content, prevent publication in the first place and keep previews private.
A trusted import or migration may write through a database connection without invoking the web app's hooks. That can be a legitimate operational workflow, but its completion must include the equivalent cache refresh and public verification.
Record the stable slug and source content before writing. Use the normal input schema, preserve tags and timestamps, and check for an existing slug before retrying an uncertain result. Do not resolve uncertainty by appending random suffixes and creating duplicate public articles.
Use a test environment and a real draft fixture. Confirm that an anonymous request cannot read the draft, an authorized editor can preview it, publication makes the public URL readable, an update refreshes body and metadata, and unpublication removes the public response. Exercise direct API requests as well as pages.
For a simple public blog, those checks are more useful than testing whether a particular loading spinner appears. They verify the actual content boundary and the cache transitions that can break it.
The Blog plugin reference covers the published operations and hooks. If your team instead needs multiple structured content types, compare Blog and CMS by content model before extending a post record into a general content system.