Framework guides

Next.js (Pages Router)

Build a statically generated blog with getStaticPaths and getStaticProps.

The Pages Router and PMKIN make a classic static blog: every post is prerendered at build time, and incremental static regeneration (ISR) picks up what you publish afterwards. Your delivery key only ever runs in getStaticProps, on the server.

On the App Router?

Follow the Next.js App Router guide instead.

  1. Install the SDK

    npm install pmkin
  2. Add your key

    Create a delivery key on your project's API page in PMKIN and put it in .env.local.

    .env.local
    PMKIN_API_KEY=your_delivery_key
  3. Create the client

    lib/pmkin.ts
    import { PmkinClient } from 'pmkin'
    const token = process.env.PMKIN_API_KEY
    if (!token) {
    throw new Error('PMKIN_API_KEY is not set. Add it to .env.local.')
    }
    export const pmkin = new PmkinClient({ token })
  4. List your documents

    getStaticProps runs at build time and, with revalidate, again in the background when the page is older than a minute.

    pages/blog/index.tsx
    import type { GetStaticProps } from 'next'
    import Link from 'next/link'
    import type { DocumentListing } from 'pmkin'
    import { pmkin } from '@/lib/pmkin'
    interface BlogProps {
    documents: DocumentListing[]
    }
    export const getStaticProps: GetStaticProps<BlogProps> = async () => {
    const documents = await pmkin.listDocuments()
    return {
    props: { documents },
    // Regenerate the page in the background at most once a minute.
    revalidate: 60
    }
    }
    export default function Blog({ documents }: BlogProps) {
    return (
    <main>
    <h1>Blog</h1>
    {documents.map((document) => (
    <article key={document.id}>
    <h2>
    <Link href={`/blog/${document.slug}`}>{document.title}</Link>
    </h2>
    <p>{document.excerpt}</p>
    </article>
    ))}
    </main>
    )
    }
  5. Render a document

    getStaticPaths lists the slugs to prerender. With fallback: 'blocking', a slug that wasn't there at build time renders on its first request. If PMKIN doesn't know the slug, return notFound: true for a 404.

    pages/blog/[slug].tsx
    import type { GetStaticPaths, GetStaticProps } from 'next'
    import Head from 'next/head'
    import type { Document } from 'pmkin'
    import { pmkin } from '@/lib/pmkin'
    interface BlogPostProps {
    document: Document
    }
    export const getStaticPaths: GetStaticPaths = async () => {
    const documents = await pmkin.listDocuments()
    return {
    paths: documents.map((document) => ({ params: { slug: document.slug } })),
    // Render documents published after the build on their first visit.
    fallback: 'blocking'
    }
    }
    export const getStaticProps: GetStaticProps<BlogPostProps, { slug: string }> = async ({
    params
    }) => {
    const document = await pmkin.findDocumentBySlug(params?.slug ?? '')
    if (!document) {
    return { notFound: true, revalidate: 60 }
    }
    return { props: { document }, revalidate: 60 }
    }
    export default function BlogPost({ document }: BlogPostProps) {
    return (
    <>
    <Head>
    <title>{document.metaTitle || document.title}</title>
    <meta
    name="description"
    content={document.metaDescription || document.excerpt}
    />
    </Head>
    <article className="prose">
    <h1>{document.title}</h1>
    <div dangerouslySetInnerHTML={{ __html: document.html }} />
    </article>
    </>
    )
    }

Rendering the HTML#

html is sanitized by PMKIN: scripts, iframes, event handlers and inline styles are removed before it reaches you, so it's safe to render with dangerouslySetInnerHTML. If you'd rather map elements to your own components, render markdown with a library like react-markdown.

Publish instantly with on-demand ISR#

To skip the minute of waiting, add an API route that revalidates the list and the post, and call it after you publish:

pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(request: NextApiRequest, response: NextApiResponse) {
if (request.headers['x-revalidate-secret'] !== process.env.REVALIDATE_SECRET) {
return response.status(401).json({ message: 'Unauthorized' })
}
const slug = String(request.query.slug ?? '')
await response.revalidate('/blog')
if (slug) {
await response.revalidate(`/blog/${slug}`)
}
return response.json({ revalidated: true })
}

Next steps#