Skip to content

Automate CI/CD for Strapi 5 + Next.js: Rebuild on Content Publish with GitHub Actions

Learn step‑by‑step how to configure Strapi 5 webhooks, a Next.js on‑demand ISR endpoint, and GitHub Actions so every published entry triggers a fresh build and deployment.

By the Revisionary team · · 4 min read

turned on MacBook Air on desk
Photo by Goran Ivos on Unsplash

When a content editor publishes an entry in Strapi 5, the site should show the change instantly without manual redeploys. You can achieve this by wiring Strapi webhooks to a CI/CD pipeline (GitHub Actions) that triggers a Next.js build and deployment. The following guide shows the full setup: Strapi webhook configuration, a secure on‑demand revalidation API in Next.js, and a GitHub Actions workflow that runs on webhook calls.

Overview of the workflow

  1. Strapi webhook fires on the entry.publish event.
  2. The webhook calls a protected API route (/api/revalidate) in the Next.js app.
  3. The API route invokes revalidatePath (or revalidateTag) to purge the ISR cache.
  4. The route also triggers a GitHub Actions workflow via the repository_dispatch event.
  5. GitHub Actions checks out the repo, runs npm install && npm run build && npm run start (or deploys to Vercel/Netlify), completing the CI/CD cycle.

The whole process runs in the Node.js runtime, because Edge runtimes cannot perform on‑demand ISR.

A computer screen with a green light on it
Photo by Milad Fakurian on Unsplash

1. Configure the Strapi webhook

Create a webhook in Strapi’s admin panel (Settings → Webhooks) or add it programmatically in config/server.ts.

ts
// ./config/server.ts
import { defineConfig } from '@strapi/types';

export default defineConfig({
  // Global webhook settings (optional)
  webhook: {
    // Add an Authorization header that will be checked by Next.js
    headers: {
      Authorization: 'Bearer MY_STRAPI_WEBHOOK_SECRET',
    },
    // Verify payload signatures (recommended)
    signature: {
      secret: process.env.STRAPI_WEBHOOK_SECRET,
    },
  },
  // Register the publish webhook
  webhook: {
    enabled: true,
    events: ['entry.publish'],
    url: `${process.env.NEXT_PUBLIC_SITE_URL}/api/revalidate`,
    // Only fire for content types that need ISR
    contentTypes: ['api::article.article', 'api::page.page'],
  },
});

Security considerations

  • Verify the Authorization header.
  • Validate the HMAC signature if you enable signature in Strapi (see the webhook signature verification example in the Strapi docs【3†L112-L136】).
  • Restrict the webhook URL to POST requests only.

2. Create a protected revalidation endpoint in Next.js

Place the file under app/api/revalidate/route.ts (App Router) and mark it as a Node.js route by adding export const runtime = 'nodejs'; – this avoids the Edge runtime limitation【8†L16-L17】.

ts
// app/api/revalidate/route.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export const runtime = 'nodejs'; // Ensure Node.js runtime for ISR

/**
 * Verify the secret sent by Strapi.
 */
function verifySecret(req: NextRequest): boolean {
  const auth = req.headers.get('authorization');
  return auth === `Bearer ${process.env.NEXT_WEBHOOK_SECRET}`;
}

export async function POST(req: NextRequest) {
  if (!verifySecret(req)) {
    return new NextResponse('Invalid secret', { status: 401 });
  }

  const payload = await req.json();
  const { model, uid, entry } = payload;

  // Determine the path(s) that need revalidation.
  // Example: article slugs are stored as entry.slug
  const paths = [] as string[];
  if (model === 'api::article.article') {
    paths.push(`/articles/${entry.slug}`);
  } else if (model === 'api::page.page') {
    paths.push(`/${entry.slug}`);
  }

  try {
    // Revalidate each path on-demand.
    for (const path of paths) {
      await revalidatePath(path);
    }
    // Trigger a GitHub Actions run via repository_dispatch.
    await triggerGitHubDispatch();
    return NextResponse.json({ revalidated: true, paths });
  } catch (err) {
    console.error('Revalidation error:', err);
    return new NextResponse('Revalidation failed', { status: 500 });
  }
}

/**
 * Call the GitHub repository_dispatch endpoint.
 */
async function triggerGitHubDispatch() {
  const token = process.env.GITHUB_TOKEN; // personal access token with repo scope
  const repo = process.env.GITHUB_REPO; // e.g. "owner/repo"
  const response = await fetch(`https://api.github.com/repos/${repo}/dispatches`, {
    method: 'POST',
    headers: {
      Accept: 'application/vnd.github+json',
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      event_type: 'strapi-publish',
      client_payload: { source: 'strapi' },
    }),
  });
  if (!response.ok) {
    throw new Error('GitHub dispatch failed');
  }
}
  • revalidatePath is the on‑demand ISR helper from Next.js【6†L226-L233】.
  • The route runs only in Node.js, satisfying the runtime requirement.
  • The triggerGitHubDispatch function sends a repository_dispatch event that GitHub Actions can listen for.

3. Set up the GitHub Actions workflow

Create .github/workflows/strapi-publish.yml in your repo.

yaml
name: Rebuild on Strapi Publish
on:
  repository_dispatch:
    types: [strapi-publish]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pages: write   # if deploying to GitHub Pages
      id-token: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Build Next.js app
        run: npm run build

      - name: Deploy to Vercel (or Netlify, etc.)
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
        run: |
          npm install -g vercel
          vercel --prod --token $VERCEL_TOKEN

How it works

  • When Strapi publishes content, the webhook calls /api/revalidate.
  • The API route validates the request, revalidates ISR paths, then posts a repository_dispatch event to GitHub.
  • GitHub Actions picks up the strapi-publish event, runs the build, and deploys the fresh static output.

4. Environment variables

VariableWhere it livesPurpose
STRAPI_WEBHOOK_SECRETStrapi .envHMAC secret for webhook signing
NEXT_WEBHOOK_SECRETNext.js .env.localSame secret used to verify the request
GITHUB_TOKENGitHub Actions secret GITHUB_TOKENPersonal access token for repository_dispatch
GITHUB_REPOGitHub Actions secret GITHUB_REPOowner/repo identifier
VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_IDGitHub Actions secretsVercel deployment credentials

5. Testing the integration locally

  1. Run Strapi: npm run develop (requires Node 18‑24 and a DB as per the requirements page)【/docs/getting-started/requirements】.
  2. Run Next.js in Node mode: npm run dev and set NEXT_PUBLIC_SITE_URL=http://localhost:3000.
  3. Use a tool like curl to POST a fake webhook payload:
    bash
    curl -X POST http://localhost:3000/api/revalidate \
         -H "Content-Type: application/json" \
         -H "Authorization: Bearer MY_STRAPI_WEBHOOK_SECRET" \
         -d '{"model":"api::article.article","uid":"article","entry":{"slug":"hello-world"}}'
  4. Verify that the console logs revalidated and that the GitHub dispatch request succeeds (check the Actions tab).

6. Optional: Tag‑based revalidation for list pages

If you have a page that lists many articles (/articles), you can tag the fetch call and revalidate the tag instead of individual paths.

ts
// app/articles/page.tsx
export const fetchCache = 'force-no-store'; // optional for fresh data
export const revalidate = 60; // fallback ISR

export default async function ArticlesPage() {
  const res = await fetch(`${process.env.STRAPI_API_URL}/articles`, {
    next: { tags: ['articles'] },
  });
  const data = await res.json();
  // render list …
}

In the webhook handler, replace revalidatePath with:

ts
await revalidateTag('articles');

This invalidates every page that used the articles tag, saving you from looping over many paths【7†L262-L268】.

7. Deploying the Next.js app

  • Vercel – the default for Next.js. The vercel CLI command in the workflow pushes the latest build.
  • Netlify – use the Netlify CLI (netlify deploy --prod) instead of Vercel commands.
  • Self‑hosted – if you run Next.js on a VPS, replace the deployment step with a SSH script that pulls the repo, runs npm ci && npm run build && pm2 restart next.

8. Observability and monitoring

  • Strapi’s audit log records each entry.publish action, giving you an immutable trail of who triggered a rebuild【12†L12-L13】.
  • Next.js automatically adds x‑nextjs‑cache response headers (HIT, STALE, MISS, REVALIDATED) to help you debug ISR behaviour【9†L23-L25】.
  • In GitHub Actions, enable the --debug flag for the Vercel CLI to capture deployment logs.

9. Common pitfalls

SymptomCauseFix
401 Invalid secret from /api/revalidateMismatched secret between Strapi and Next.jsEnsure both STRAPI_WEBHOOK_SECRET and NEXT_WEBHOOK_SECRET are identical and loaded from env files.
ISR does not run on Edge runtimeUsing Edge instead of Node.js runtimeAdd export const runtime = 'nodejs'; to the route file.
GitHub dispatch fails with 404Wrong owner/repo or missing token scopesVerify GITHUB_REPO and grant repo scope to the PAT.
Build times are longUncached node_modules in CIAdd a caching step: actions/cache@v4 for ~/.npm or node_modules.
A server room with rows of black and white cabinets under blue cables
Photo by Taylor Vick on Unsplash

10. Extending the pipeline

  • Content versioning – If you use the Revisionary plugin, you can also listen to entry.unpublish and entry.delete events to clean up stale pages.
  • Preview builds – Combine the webhook with Vercel preview deployments to generate a draft URL for editors.
  • Feature flags – Pass a preview flag in the payload and have the Next.js route decide whether to revalidate the live site or a preview deployment.

By wiring Strapi 5 webhooks to a protected Next.js on‑demand ISR endpoint and a GitHub Actions workflow, you achieve a fully automated CI/CD loop: every publish instantly invalidates the static cache and triggers a fresh build. The approach respects security best practices, works with TypeScript, and can be extended with Revisionary’s audit log for full traceability.


Further reading

TagsCI/CDGitHub ActionsStrapiNext.jsTypeScriptWebhooks

Sources

  1. Strapi webhook documentation
  2. Next.js Incremental Static Regeneration guide
  3. Next.js Edge runtime limitations

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