Getting started
Install policykit, write your first policy revision on disk, and render it in a Next.js App Router page - in about five minutes.

By the end of this page you have a terms-of-service revision stored on disk and rendered at
/en/legal/terms in your Next.js app.
Prerequisites: a Next.js App Router app with React 18 or newer. The package's peer range starts
at Next 14; the route sample below uses the async params shape, which is Next 15 and up.
Install the package
npm install @daanvandenbergh/policykit next-mdx-remote
next, react, and react-dom are peer dependencies you already have. next-mdx-remote (v5 or
v6) is the fourth peer, and it is only needed for the @daanvandenbergh/policykit/react subpath -
the part that renders MDX. The root entry imports nothing but node:fs, node:path, and
gray-matter.
Create your first revision
A revision IS a directory named after the day you decided it. Create one file inside it, named after the locale:
policies/terms-of-service/
2026-08-11/
en.mdx
Write policies/terms-of-service/2026-08-11/en.mdx. The default locale's file carries the
revision's metadata: effectiveFrom, notice, and changeSummary are required, title is
optional.
---
effectiveFrom: 2026-08-11
notice: none
changeSummary: "First published terms of service."
title: "Terms of Service"
---
## 1. Agreement
By using the service you agree to these terms.
effectiveFrom- a realYYYY-MM-DDcalendar day, on or after the revision date. Here they match, so the text binds immediately.notice-none,notify, orreconsent. A first publish owes nobody notice.changeSummary- a non-empty sentence. Internal evidence, never rendered to users.title- optional.
The body must not be empty: the MDX body is the legal text, so frontmatter alone is an error.
Configure the policy
One instance per policy, in one module. dir must be absolute - the constructor rejects a
relative path, because the same instance runs from several working directories and would otherwise
point at different content per cwd.
// src/content/policies.ts
import path from "node:path";
import { Policy } from "@daanvandenbergh/policykit";
export const termsPolicy = new Policy({
slug: "terms-of-service",
dir: path.join(process.cwd(), "policies", "terms-of-service"),
locales: ["en"],
});
locales is required - it is the set of languages you serve, and every locale file on disk must
belong to it. defaultLocale is optional: it defaults to "en" and must be one of locales, so a
single-locale English policy never sets it.
Render it
PolicyDocument is an async server component that renders one revision's MDX body. Omit
revision and it picks the live-page default itself: the revision that binds today, or the newest
published one if none binds yet.
// app/[locale]/legal/terms/page.tsx
import { PolicyDocument } from "@daanvandenbergh/policykit/react";
import { termsPolicy } from "@/content/policies";
export default async function TermsPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
return (
<article>
<PolicyDocument policy={termsPolicy} locale={locale} />
</article>
);
}
Run npm run dev and open /en/legal/terms. You should see the heading "1. Agreement" and the
sentence below it. That is your MDX body, compiled and served.
Nothing is styled and no title is printed - PolicyDocument renders the body only, and the page
shell stays yours.
This route serves one locale, so it renders as written. Once the [locale] segment can hold a
language the policy does not have, guard before rendering: PolicyDocument throws when the
resolved revision has no file for that locale. Rendering policies has the
guard-then-404 recipe.
Related
When you publish your second version, writing a revision walks the authoring
steps - or install /policykit, the Claude Code skill that runs those steps for you.
For titles, archive routes, and the MDX component map, see
rendering policies.