Skip to content

ISR with Strapi and Next.js: A step‑by‑step guide to version‑safe static regeneration

Learn how to combine Next.js Incremental Static Regeneration with Strapi's Revisionary plugin to serve fresh content, respect editorial locks, and keep a full version history.

By the Revisionary team · · 4 min read

laptop screen displaying colorful code
Photo by Mohammad Rahmani on Unsplash

Incremental Static Regeneration (ISR) lets a Next.js site serve pre‑rendered pages and update them in the background without a full rebuild. When the data source is a headless CMS like Strapi, you must coordinate the regeneration trigger with the editorial workflow so that unpublished drafts, edit locks, or content restores are not served unintentionally. This guide shows how to wire ISR to Strapi’s Revisionary plugin, fetch only released versions, and keep the full version history available for audit and rollback.

1. Prerequisites

RequirementDetails
Strapi instanceRunning Strapi with the Revisionary plugin installed (see the Install guide)
Next.js projectUsing TypeScript, Node.js LTS, and the next package
API accessContent API endpoint enabled, with authentication token that has read‑only rights
DeploymentVercel, Netlify, or any platform that respects Next.js ISR headers

Make sure the Revisionary plugin is active so every content change is recorded. The plugin exposes the version history through the regular Content API, so the front end can request the published version while still being able to query past versions for debugging or preview.

Employer dashboard showing application trends and key metrics
Photo by prashant hiremath on Unsplash

2. Fetching only the published version

Strapi stores drafts, scheduled publications, and the historic snapshots created by Revisionary. To avoid serving a draft, query the API with the publicationState filter set to live.

ts
// lib/strapi.ts
import fetch from 'node-fetch'

const API_URL = process.env.STRAPI_URL!
const API_TOKEN = process.env.STRAPI_TOKEN!

export async function fetchLive<T>(endpoint: string): Promise<T> {
  const url = new URL(`${API_URL}/api/${endpoint}`)
  // `publicationState=live` forces Strapi to return only published entries
  url.searchParams.append('publicationState', 'live')
  const res = await fetch(url.toString(), {
    headers: { Authorization: `Bearer ${API_TOKEN}` },
  })
  if (!res.ok) {
    throw new Error(`Strapi request failed: ${res.status}`)
  }
  const { data } = await res.json()
  return data as T
}

All page‑level data fetching functions should use fetchLive so that ISR revalidation always pulls the same stable snapshot that editors have published.

3. Adding ISR to a page component

Next.js supports ISR via the revalidate property returned from getStaticProps. Combine it with the live fetch from the previous step.

tsx
// pages/[slug].tsx
import { GetStaticPaths, GetStaticProps } from 'next'
import { fetchLive } from '@/lib/strapi'
import type { Article } from '@/types'

export const getStaticPaths: GetStaticPaths = async () => {
  const articles = await fetchLive<Article[]>('articles')
  const paths = articles.map(a => ({ params: { slug: a.attributes.slug } }))
  return { paths, fallback: 'blocking' }
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const slug = params?.slug as string
  const article = await fetchLive<Article>(`articles?filters[slug][$eq]=${slug}`)
  if (!article) return { notFound: true }

  return {
    props: { article },
    // Re‑generate the page at most once every 60 seconds
    revalidate: 60,
  }
}

export default function ArticlePage({ article }: { article: Article }) {
  return (
    <article>
      <h1>{article.attributes.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.attributes.body }} />
    </article>
  )
}

The revalidate: 60 line tells Next.js to keep the page fresh for up to a minute. Adjust the interval based on your editorial cadence.

4. Respecting edit locks and reviews

Revisionary introduces edit locks to prevent two editors from overwriting each other and a Review space for content awaiting approval. When an entry is locked or in review, you should not trigger a regeneration. Instead, let the lock stay until the reviewer publishes.

4.1 Detecting a lock via the API

Revisionary adds a locked flag and a reviewStatus field to the entry meta. Query these fields alongside the normal content.

ts
export async function fetchArticleMeta(slug: string) {
  const url = new URL(`${API_URL}/api/articles`)
  url.searchParams.append('filters[slug][$eq]', slug)
  url.searchParams.append('fields', 'locked,reviewStatus')
  url.searchParams.append('publicationState', 'live')
  const res = await fetch(url.toString(), {
    headers: { Authorization: `Bearer ${API_TOKEN}` },
  })
  const { data } = await res.json()
  return data[0]?.attributes ?? null
}

4.2 Conditional revalidation

Next.js lets you programmatically revalidate a path from an API route. Combine the lock check with a webhook that Strapi fires on publish.

ts
// pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { fetchArticleMeta } from '@/lib/strapi'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const token = req.headers['x-webhook-token']
  if (token !== process.env.STRAPI_WEBHOOK_TOKEN) {
    return res.status(401).end('Invalid token')
  }

  const { slug } = req.body
  const meta = await fetchArticleMeta(slug)
  if (meta.locked || meta.reviewStatus !== 'approved') {
    // Skip revalidation; the content is not ready for public consumption
    return res.status(200).json({ revalidated: false, reason: 'locked or pending review' })
  }

  try {
    await res.revalidate(`/${slug}`)
    return res.json({ revalidated: true })
  } catch (err) {
    return res.status(500).json({ error: 'Revalidation failed' })
  }
}

Configure Strapi to call this endpoint on the afterPublish lifecycle event. The webhook payload should contain the slug of the published entry.

5. Leveraging the version history for preview

Even though ISR serves only the live version, you can still provide a preview mode that reads a specific revision. Revisionary stores each version under a versions relationship; the Content API can retrieve it with a populate[versions] query.

ts
export async function fetchVersion(slug: string, versionId: number) {
  const url = new URL(`${API_URL}/api/articles`)
  url.searchParams.append('filters[slug][$eq]', slug)
  url.searchParams.append('populate[versions][filters][id][$eq]', versionId.toString())
  const res = await fetch(url.toString(), {
    headers: { Authorization: `Bearer ${API_TOKEN}` },
  })
  const { data } = await res.json()
  return data[0]?.attributes?.versions?.data[0]?.attributes ?? null
}

Expose a preview route that sets previewData (Next.js preview mode) and renders the selected version. This lets editors see exactly what will be published without affecting the ISR cache.

6. CI/CD integration

A robust pipeline validates that the Strapi schema, Revisionary settings, and Next.js ISR configuration stay in sync.

  1. Lint & type‑check – Run eslint and tsc on both the Strapi plugins folder and the Next.js code.
  2. Schema test – Use a script that fetches the Content API schema endpoint and asserts required fields exist.
  3. Version‑lock test – Simulate a draft edit lock via the API and verify the revalidation webhook returns revalidated: false.
  4. Build – Build the Next.js app (next build). The build will fail if any getStaticProps throws due to missing live data.
  5. Deploy – Deploy to Vercel (or your host). Vercel automatically respects the revalidate intervals.

Add the following GitHub Actions snippet as an example:

yaml
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Node
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - name: Verify Strapi schema
        env:
          STRAPI_URL: ${{ secrets.STRAPI_URL }}
          STRAPI_TOKEN: ${{ secrets.STRAPI_TOKEN }}
        run: node scripts/verify-schema.js
      - run: npm run build

7. Observability and logging

Revisionary writes every lock, restore, and trash action to the audit log. Forward these logs to your preferred observability platform (e.g., Loki, Datadog) by enabling Strapi’s logger transport. Combine the audit log with Next.js request logs to trace the full path from an editor’s action to the moment a page is regenerated.

js
// config/logger.js (Strapi)
module.exports = ({ env }) => ({
  transports: [
    {
      level: 'info',
      transport: {
        target: 'pino-pretty',
        options: { destination: './logs/combined.log' },
      },
    },
    // Add a HTTP transport to ship logs to an external system
    {
      level: 'warn',
      transport: {
        target: 'pino-http',
        options: { url: env('LOGGING_ENDPOINT') },
      },
    },
  ],
})

Monitoring the locked and reviewStatus fields in real time helps you detect when a content freeze is unintentionally preventing ISR updates.

8. Putting it all together

  1. Configure Revisionary – Choose which content types are versioned via the Choose what is versioned page.
  2. Expose live API – Use the publicationState=live filter in every Next.js fetch.
  3. Set up webhook – Point Strapi’s afterPublish webhook to /api/revalidate.
  4. Implement lock check – The revalidation handler skips pages that are still under edit or review.
  5. Add preview – Use the version‑history endpoint to render drafts for internal reviewers.
  6. Automate CI/CD – Run lint, type‑check, schema verification, and build steps on every commit.
  7. Log and observe – Forward Revisionary audit logs and Next.js request logs to a central observability tool.

Following these steps gives you a fast, cache‑friendly front end that never leaks unpublished drafts, while preserving the full editorial audit trail provided by Revisionary.

9. Common pitfalls

SymptomLikely causeFix
ISR serves stale content after a publishWebhook token mismatchEnsure STRAPI_WEBHOOK_TOKEN matches the token configured in Strapi
404 on preview URLMissing previewData flagCall res.setPreviewData({}) before rendering preview page
Audit log empty for locksRevisionary not enabled on the content typeEnable versioning for the type in Choose what is versioned
Revalidation loop (continuous rebuilds)publicationState omitted, fetching draftsAlways add publicationState=live to API calls
a rack of servers in a server room
Photo by Kevin Ache on Unsplash

10. Conclusion

Incremental Static Regeneration gives you the performance of static pages while keeping your site fresh. When the data source is a version‑aware headless CMS like Strapi, the extra step is to respect editorial states—drafts, locks, and reviews—so that only approved content reaches the cache. By filtering for the live publication state, checking Revisionary’s lock metadata, and wiring a webhook that conditionally triggers revalidation, you achieve a safe, observable, and CI‑ready workflow. The same Content API also lets you build preview experiences that pull any historic version, giving editors confidence that their changes are correctly captured before they go live.

TagsNext.jsISRStrapiRevisionaryVersioningTypeScript

The Revisionary team

Developers who build Revisionary, a version history plugin for Strapi 5. We write about Strapi, web development, AI and application security.

This article was written with the help of AI.

All articles