LIVE Session: Learn how to manage your entire website from ChatGPT & ClaudeSave your spot
Product News
·5 min read

How to Use Prismic with the Next.js App Router

In this tutorial, you'll build a small Next.js website with Prismic from scratch: setting up the project, modeling content, fetching and displaying it, wiring up live and full-website previews, and configuring caching so your site stays fast without serving stale content.

What you should already know

This tutorial assumes some familiarity with Next.js and React. If either is new to you, the official Next.js docs and React's Learn React are great places to start.

Set up your Next.js project

You can add Prismic to a new or an existing Next.js project.

npx create-next-app@latest my-website
cd my-website

From your project's root, run the Prismic CLI's init command. It creates your repository (if you don't have one yet), installs the required packages, and configures your project.

npx prismic init

# Already have a repository? Point the CLI at it directly:
npx prismic init --repo your-domain

What prismic init sets up for you

Running the CLI configures your project with:

  • A Prismic client at prismicio.ts
  • Route resolvers and Next.js fetch options preconfigured on that client
  • A /slice-simulator route for live previews in the Page Builder
  • /api/preview and /api/exit-preview route handlers for full-website draft previews
  • A /api/revalidate route handler for on-demand cache revalidation

You'll use every one of these in this tutorial.

Get familiar with the Prismic CLI

The Prismic CLI is the tool you'll return to throughout a project's life, not just at setup. It manages your repository's locales, access tokens, previews, and webhooks directly from your terminal, which is especially handy if you're wiring up a project with an AI coding agent.

# Regenerate any setup files your project is missing
npx prismic gen setup

# Create a private-content access token
npx prismic token create

# Register a preview URL
npx prismic preview add http://localhost:3000/api/preview --name Development

# Create a revalidation webhook
npx prismic webhook create https://example.com/api/revalidate --trigger documentsPublished --trigger documentsUnpublished

# Add a locale
npx prismic locale add fr-fr

Model content with page types and slices

Content writers build pages from page types, like a homepage, blog post, or landing page, and reusable sections called slices, like a hero, a text block, or a call to action. You can model both by hand in the Type Builder, or hand the job to the Prismic CLI, which is built for AI agents to drive directly.

TypeScript types are generated automatically, and a starter React component is scaffolded for every slice you create at src/slices/<SliceName>/index.tsx.

Generated TypeScript types

Every page type and slice you model is reflected in a generated prismicio-types.d.ts file, kept in sync automatically as your content models change. These types cover your Content.PageDocument, Content.HomepageDocument, and every slice's props, so your editor can autocomplete field names and catch typos before you ship them.

Define your routes

Prismic needs to know your website's URL structure to fill in link fields correctly. The CLI keeps a routes array in prismic.config.json in sync with your page types automatically: a page type named Homepage maps to /, a page type named Page maps to /:uid, and any other page type maps to /<api-id>/:uid.

Edit the file directly if you need custom paths. Just make sure they match your Next.js file-system routes.

{
  "repositoryName": "example-prismic-repo",
  "routes": [
    { "type": "homepage", "path": "/" },
    { "type": "page", "path": "/:uid" },
    { "type": "blog", "path": "/blog/:uid" }
  ]
}

Fetch and display content

Your prismicio.ts file centralizes the Prismic client, your routes, and your caching configuration in one place. You'll import createClient() from here anywhere you need to query content.

import {
  createClient as baseCreateClient,
  type ClientConfig,
} from "@prismicio/client";
import { enableAutoPreviews } from "@prismicio/next";
import prismicConfig from "../prismic.config.json";

export const repositoryName = prismicConfig.repositoryName;

export const createClient = (config: ClientConfig = {}) => {
  const client = baseCreateClient(repositoryName, {
    routes: prismicConfig.routes,
    fetchOptions: {
      next: { tags: ["prismic"] },
      cache: "force-cache",
    },
    ...config,
  });

  enableAutoPreviews({ client });

  return client;
};

With the client in place, query content directly inside an async Server Component. Note that in current Next.js versions, route params arrive as a Promise, so you'll need to await them before use.

import type { Metadata } from "next";
import { SliceZone } from "@prismicio/react";
import { createClient } from "@/prismicio";
import { components } from "@/slices";

export default async function Page({ params }: PageProps<"/[uid]">) {
  const { uid } = await params;
  const client = createClient();
  const page = await client.getByUID("page", uid);

  return <SliceZone slices={page.data.slices} components={components} />;
}

export async function generateMetadata({
  params,
}: PageProps<"/[uid]">): Promise<Metadata> {
  const { uid } = await params;
  const client = createClient();
  const page = await client.getByUID("page", uid);

  return {
    title: page.data.meta_title,
    description: page.data.meta_description,
  };
}

export async function generateStaticParams() {
  const client = createClient();
  const pages = await client.getAllByType("page");

  return pages.map((page) => ({ uid: page.uid }));
}

You can query content the same way inside a slice component, which is useful for things like a footer or a contact form that needs its own Settings document.

A handful of components from @prismicio/react and @prismicio/next handle the rest of the rendering work:

  • <SliceZone> renders a page's slices using the components you map them to
  • <PrismicRichText> renders rich text fields as React elements
  • <PrismicNextLink> renders link fields using next/link
  • <PrismicNextImage> renders image fields using next/image, with automatic width and height

Add live previews in the Page Builder

The Page Builder shows a live-updating thumbnail for each slice as content writers edit, powered by a slice simulator route that prismic init already added to your project.

import {
  SliceSimulator,
  SliceSimulatorParams,
  getSlices,
} from "@prismicio/next";
import { SliceZone } from "@prismicio/react";
import { components } from "@/slices";

export default async function SliceSimulatorPage({
  searchParams,
}: SliceSimulatorParams) {
  const { state } = await searchParams;
  const slices = getSlices(state);

  return (
    <SliceSimulator>
      <SliceZone slices={slices} components={components} />
    </SliceSimulator>
  );
}

Point your repository at the simulator so the Page Builder knows where to load it:

npx prismic preview set-simulator http://localhost:3000

Set up full-website draft previews

Full-website previews let content writers click through your actual site to see draft content in context before it's published. <PrismicPreview> adds the toolbar and event listeners that make this work.

import { type ReactNode } from "react";
import { PrismicPreview } from "@prismicio/next";
import { repositoryName } from "@/prismicio";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <PrismicPreview repositoryName={repositoryName} />
      </body>
    </html>
  );
}

Two Route Handlers bridge Prismic and your app: one enters Next.js Draft Mode and redirects to the previewed page, the other ends the session.

import { NextRequest } from "next/server";
import { redirectToPreviewURL } from "@prismicio/next";
import { createClient } from "@/prismicio";

export async function GET(request: NextRequest) {
  const client = createClient();

  return await redirectToPreviewURL({ client, request });
}
import { exitPreview } from "@prismicio/next";

export function GET() {
  return exitPreview();
}

Finally, tell Prismic about your preview endpoint. Add another one for your production domain once you deploy.

npx prismic preview add http://localhost:3000/api/preview --name Development

Configure caching and on-demand revalidation

Look back at the fetchOptions in your prismicio.ts file: next.tags tags every Prismic API call with "prismic", and cache: "force-cache" tells Next.js to cache those calls indefinitely, until that tag is revalidated. This gives you the speed of static generation without giving up fresh content.

To clear the cache the moment content changes, add a /api/revalidate Route Handler that calls revalidateTag() on the same "prismic" tag.

import { NextResponse } from "next/server";
import { revalidateTag } from "next/cache";

export async function POST() {
  revalidateTag("prismic");

  return NextResponse.json({ revalidated: true, now: Date.now() });
}

Then register that endpoint as a webhook, so Prismic calls it whenever a document is published or unpublished. You don't need to configure anything with your hosting provider.

npx prismic webhook create https://example.com/api/revalidate \
  --trigger documentsPublished \
  --trigger documentsUnpublished

Working locally

Since queries are cached indefinitely, you won't see new content locally until you revalidate or hard-refresh your browser (Shift+Cmd/Ctrl+R). That's expected. Once deployed, the webhook keeps production content current automatically.

Migrating an older Prismic and Next.js project

If you built with Prismic and Next.js a while back, here's what's changed and what to check when you touch that codebase again:

  1. Pages Router to App Router: getStaticProps and getStaticPaths are replaced by fetching data directly inside async Server Components, plus generateStaticParams for static generation.
  2. Slice Machine to the Prismic CLI: @slicemachine/init and the standalone Slice Machine UI have given way to npx prismic init and CLI commands like prismic gen setup. The Type Builder remains available if you prefer a visual editor.
  3. slicemachine.config.json to prismic.config.json: routes, repository name, and adapter options moved to the new config file. Older projects can keep their existing file; new projects get the new one by default.
  4. Route params are now asynchronous: params and searchParams are Promises in current Next.js versions, so page and layout components need to await params before reading uid, lang, or other route segments.
  5. middleware.ts is now proxy.ts: if your project uses internationalization or other request-level logic, rename the file and update the exported function to proxy().

If you're not sure what's missing from an older project, npx prismic gen setup will scaffold any files it doesn't find, including the Prismic client, the slice simulator, and the preview and revalidate Route Handlers.

Recap

You now have a Next.js website with Prismic wired in end to end:

  • Content is modeled through the Prismic CLI or the Type Builder, with TypeScript types generated for you
  • Pages and slices fetch content directly inside Server Components
  • Content writers get live previews in the Page Builder and full-website draft previews before anything publishes
  • Published content reaches your site through tagged, on-demand cache revalidation
  • More about Next.js at https://prismic.io/docs/nextj

Questions?

If something doesn't click, ask in the Prismic Community forum (we're there most days). Or add your comments below.

Article written by

Angelo Ashmore

Senior Developer Experience Engineer

More posts

8 comments

Ben

This setup is super clean. Love it
Reply·3 years ago

Alison from Prismic

This is a reply to Ben's comment

This setup is super clean. Love it
Thanks for sharing, Ben! We are glad you think so! 😀
Reply·3 years ago

Vagiz

Hello In the article, it mentions setting up on-demand revalidation in Prismic, and it specifies using the URL https://example.com/revalidate. However, please be aware that the accurate URL should be https://example.com/api/revalidate. Please make this correction for the accurate configuration. Thank you)
Reply·3 years ago

Lea from Prismic

This is a reply to Vagiz's comment

Hello In the article, it mentions setting up on-demand revalidation in Prismic, and it specifies using the URL https://example.com/revalidate. However, please be aware that the accurate URL should be https://example.com/api/revalidate. Please make this correction for the accurate configuration. Thank you)
Thanks for the callout, Vagiz! You are correct! 😀 This section is now updated.
Reply·3 years ago

Jake

Nice article. But wouldn't this mean that anyone could post to the revalidate endpoint and revalidate the Next cache? What if I wanted to verify that the request is coming from Prismic, how would I approach this?

Reply·2 years ago

Samuel

This is a reply to Jake's comment

Nice article. But wouldn't this mean that anyone could post to the revalidate endpoint and revalidate the Next cache? What if I wanted to verify that the request is coming from Prismic, how would I approach this?

Hey Jake!

That's a very good question!

When you create or edit your webhook in the Prismic UI, you have the ability to set a secret. This will make the body of the hook look something like:

{

"type": "Webhook Name",

"domain": "prismic-repo-name",

"apiUrl": "https://prismic-repo-name.prismic.io/api",

"secret": "this-is-your-key"

}

You can extract that data in the revalidation route at src/app/api/revalidate/route.ts (Documented here).

And then wrap your revalidation call in a conditional like:

if (requestdata.secret === "this-is-your-key") {

revalidateTag('prismic')

return NextResponse.json({ revalidated: true, now: Date.now() })

}

Hope this helps!

All the best / Samuel

Reply·2 years ago

Yann

Hey, for the revalidation route, I think it should use GET instead of POST as the method:

export async function GET() {

Otherwise it won't work with prismic webhooks.

Reply·2 years ago

Samuel

This is a reply to Yann's comment

Hey, for the revalidation route, I think it should use GET instead of POST as the method:

export async function GET() {

Otherwise it won't work with prismic webhooks.

Hey Yann, Prismic webhooks should send a POST request. If the webhook isn't working as expected, could you open a thread on our forum so we can check it out together?

All the best / Samuel

Reply·2 years ago
Hit your website goals

Websites success stories from the Prismic Community

How Arcadia is Telling a Consistent Brand Story

Read Case Study

How Evri Cut their Time to Ship

Read Case Study

How Pallyy Grew Daily Visitors from 500 to 10,000

Read Case Study

From Powder to Pixels - Perfectly Planned Ski Vacations, Now Perfectly Digital

Read Case Study