@dispatchcms/next
v0.0.6
Published
Fetch published posts from a Dispatch CMS in Next.js
Maintainers
Readme
@dispatchcms/next
Fetch published posts from a Dispatch CMS in your Next.js app.
Install
npm install @dispatchcms/next
# or
pnpm add @dispatchcms/next
# or
yarn add @dispatchcms/nextEnvironment variables
Set this in your Next.js project (e.g. .env.local) if you don't call initDispatch:
| Variable | Description |
|----------|-------------|
| NEXT_PUBLIC_DISPATCH_SITE_KEY | Your site key (e.g. pk_xxxx). Required if you don't call initDispatch. |
Setup
Call initDispatch once so the client knows your site key. For example in your root layout or before any fetch:
import { initDispatch } from "@dispatchcms/next";
initDispatch({
siteKey: process.env.NEXT_PUBLIC_DISPATCH_SITE_KEY!,
});You can omit initDispatch entirely if NEXT_PUBLIC_DISPATCH_SITE_KEY is set; the package will use it automatically.
Caching
The package does not cache responses in memory. Each call to getPosts() or getPost(slug) fetches from the API, so your app always sees up-to-date data (e.g. when you unpublish a post in the CMS it disappears on the next load). For performance, rely on Next.js: use Server Components and the default fetch caching, or revalidate / ISR, so that responses are cached at the request level and stay fresh according to your revalidation settings.
API
getPosts(siteKey?)
Returns all published posts for the site. Always fetches from the API (no in-memory cache).
- Returns:
Promise<Post[]> - Optional: pass
siteKeyto override the configured site for this call.
getPost(slug, siteKey?)
Returns a single published post by slug, or null if not found. Always fetches from the API (no in-memory cache).
- Returns:
Promise<Post | null> - Optional: pass
siteKeyas the second argument to override the configured site.
getPostByPreviewToken(token)
Returns a single post by its preview token (draft or published). Use this in your app’s preview route so editors can open a shareable link and see the post as it will appear. No site key is required; the token is the secret.
- Returns:
Promise<Post | null> - Example: Implement a route at
/preview(or/blog/preview) that readstokenfrom the query and renders the post with the same layout as your live post page (see below).
Types
Exportable types:
Post– A post from the CMS. Main fields:id,title,slug,content(TipTap/ProseMirror JSON),excerpt,featured_image,published,published_at,created_at,updated_at,site_id. See Rendering content below for how to rendercontent.DispatchConfig– Options forinitDispatch:{ siteKey: string }.
Use your editor’s type hints or the generated .d.ts for the full shape.
Rendering content
Post content is TipTap (ProseMirror) JSON. Render it with TipTap’s static renderer so formatting, links, and images match the CMS:
- Server Components: Use
renderToHTMLStringfrom@tiptap/static-renderer/pm/html-stringwith the same extensions as the CMS (e.g. Document, Paragraph, Heading, Bold, Italic, Blockquote, BulletList, ListItem, Link, Image), then render the HTML in a wrapperdiv(the output is safe TipTap-generated HTML). - Client: Use
renderToReactElementfrom@tiptap/static-renderer/pm/reactfor a React node with nodangerouslySetInnerHTML.
Use the same extensions as the CMS editor for full parity. See TipTap static renderer docs.
Example (Next.js App Router)
List posts (Server Component):
// app/blog/page.tsx
import { getPosts } from "@dispatchcms/next";
export default async function BlogPage() {
const posts = await getPosts();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
);
}Single post (Server Component):
// app/blog/[slug]/page.tsx
import { getPost } from "@dispatchcms/next";
import { notFound } from "next/navigation";
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
{post.excerpt && <p>{post.excerpt}</p>}
{/* Render post.content with TipTap: renderToHTMLString(extensions, post.content) then a div, or use renderToReactElement in a client component */}
</article>
);
}The CMS API only returns published posts; drafts are not included.
Preview route
To support public preview links from the CMS (e.g. https://yoursite.com/preview?token=xxx), add a preview page that fetches by token and renders the post:
// app/preview/page.tsx
import { getPostByPreviewToken } from "@dispatchcms/next";
import { notFound } from "next/navigation";
export default async function PreviewPage({
searchParams,
}: {
searchParams: Promise<{ token?: string }>;
}) {
const { token } = await searchParams;
if (!token) notFound();
const post = await getPostByPreviewToken(token);
if (!post) notFound();
return (
<article>
<p className="text-sm text-muted-foreground">Preview</p>
<h1>{post.title}</h1>
{post.excerpt && <p>{post.excerpt}</p>}
{/* Render post.content with TipTap (same as your live post page) */}
</article>
);
}In the CMS, set Site URL in Site settings to your site’s base URL (e.g. https://yoursite.com). The preview link will use that URL plus /preview?token=....
