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
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
| Requirement | Details |
|---|---|
| Strapi instance | Running Strapi with the Revisionary plugin installed (see the Install guide) |
| Next.js project | Using TypeScript, Node.js LTS, and the next package |
| API access | Content API endpoint enabled, with authentication token that has read‑only rights |
| Deployment | Vercel, 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.
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.
// 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.
// 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.
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.
// 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.
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.
- Lint & type‑check – Run
eslintandtscon both the Strapi plugins folder and the Next.js code. - Schema test – Use a script that fetches the Content API
schemaendpoint and asserts required fields exist. - Version‑lock test – Simulate a draft edit lock via the API and verify the revalidation webhook returns
revalidated: false. - Build – Build the Next.js app (
next build). The build will fail if anygetStaticPropsthrows due to missing live data. - Deploy – Deploy to Vercel (or your host). Vercel automatically respects the
revalidateintervals.
Add the following GitHub Actions snippet as an example:
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 build7. 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.
// 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
- Configure Revisionary – Choose which content types are versioned via the Choose what is versioned page.
- Expose live API – Use the
publicationState=livefilter in every Next.js fetch. - Set up webhook – Point Strapi’s afterPublish webhook to
/api/revalidate. - Implement lock check – The revalidation handler skips pages that are still under edit or review.
- Add preview – Use the version‑history endpoint to render drafts for internal reviewers.
- Automate CI/CD – Run lint, type‑check, schema verification, and build steps on every commit.
- 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
| Symptom | Likely cause | Fix |
|---|---|---|
| ISR serves stale content after a publish | Webhook token mismatch | Ensure STRAPI_WEBHOOK_TOKEN matches the token configured in Strapi |
| 404 on preview URL | Missing previewData flag | Call res.setPreviewData({}) before rendering preview page |
| Audit log empty for locks | Revisionary not enabled on the content type | Enable versioning for the type in Choose what is versioned |
| Revalidation loop (continuous rebuilds) | publicationState omitted, fetching drafts | Always add publicationState=live to API calls |
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.
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.