Framework guides

Remix

Load PMKIN documents in Remix loaders.

Remix loaders run on the server, so they're the perfect place to talk to PMKIN: your key stays private, and pages arrive fully rendered. (This very site is a Remix app that reads its docs from PMKIN.)

  1. Install the SDK

    npm install pmkin
  2. Add your key

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

    .env
    PMKIN_API_KEY=your_delivery_key
  3. Create the client

    The .server suffix keeps the module, and your key, out of the browser bundle.

    app/pmkin.server.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.')
    }
    export const pmkin = new PmkinClient({ token })
  4. List your documents

    app/routes/blog._index.tsx
    import { json } from '@remix-run/node'
    import { Link, useLoaderData } from '@remix-run/react'
    import { pmkin } from '~/pmkin.server'
    export async function loader() {
    const documents = await pmkin.listDocuments()
    return json(
    { documents },
    // Let a CDN serve the list for a minute, then refresh it in the background.
    { headers: { 'Cache-Control': 'public, max-age=60, stale-while-revalidate=600' } }
    )
    }
    export default function Blog() {
    const { documents } = useLoaderData<typeof loader>()
    return (
    <main>
    <h1>Blog</h1>
    {documents.map((document) => (
    <article key={document.id}>
    <h2>
    <Link prefetch="intent" to={`/blog/${document.slug}`}>
    {document.title}
    </Link>
    </h2>
    <p>{document.excerpt}</p>
    </article>
    ))}
    </main>
    )
    }
  5. Render a document

    Throw a 404 Response when the slug isn't found, so search engines see a real 404 instead of an empty page.

    app/routes/blog.$slug.tsx
    import { json, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node'
    import { useLoaderData } from '@remix-run/react'
    import { pmkin } from '~/pmkin.server'
    export async function loader({ params }: LoaderFunctionArgs) {
    const document = await pmkin.findDocumentBySlug(params.slug ?? '')
    if (!document) {
    throw new Response('Not found', { status: 404 })
    }
    return json(
    { document },
    { headers: { 'Cache-Control': 'public, max-age=60, stale-while-revalidate=600' } }
    )
    }
    export const meta: MetaFunction<typeof loader> = ({ data }) => {
    if (!data) {
    return [{ title: 'Not found' }]
    }
    const { document } = data
    return [
    { title: document.metaTitle || document.title },
    { name: 'description', content: document.metaDescription || document.excerpt }
    ]
    }
    export default function BlogPost() {
    const { document } = useLoaderData<typeof loader>()
    return (
    <article className="prose">
    <h1>{document.title}</h1>
    <div dangerouslySetInnerHTML={{ __html: document.html }} />
    </article>
    )
    }
  6. Handle errors

    Add an error boundary to the same route for the 404 and for the rare failed request:

    app/routes/blog.$slug.tsx
    import { isRouteErrorResponse, useRouteError } from '@remix-run/react'
    export function ErrorBoundary() {
    const error = useRouteError()
    if (isRouteErrorResponse(error) && error.status === 404) {
    return <h1>We couldn’t find that post.</h1>
    }
    return <h1>Something went wrong loading this post. Try again in a minute.</h1>
    }

Rendering the HTML#

PMKIN sanitizes html before it reaches you: scripts, iframes, event handlers and inline styles are removed. Render it with dangerouslySetInnerHTML and style it with the Tailwind Typography plugin's prose class, or render markdown yourself if you want custom components.

Caching#

Published content changes rarely, so cache it. The Cache-Control headers above let a CDN serve pages for a minute and refresh them in the background, which keeps you far from the rate limit even under heavy traffic.

Don't cache draft previews. If you fetch with includeDrafts, send Cache-Control: private, no-store.

Next steps#