Tomek Juranek
Mar 25, 2025
  2367
(0 votes)

Revalidate Page in Next.js after Publishing Content in Headless Optimizely PaaS CMS.

Headless CMS implementations are becoming increasingly popular. In this approach, the CMS and the front-end application are decoupled and can use different technology stacks. However, when building such a website, it is essential to ensure a seamless connection between the CMS and the front-end to maintain a smooth editing experience.

One of the most popular framework to implement head application for the headless CMS is Next.js, which offers different rendering techniques: client or server side rendering, static site generation and incremental static regeneration. We will not cover all of them here, if you need more information check out this article. In this blog we will focus on Incremental Static Regeneration (ISR), which is the way to go for majority of cases when working with CMS. ISR combines the ability of pre-rendering content to HTML files from Static Site Generation and flexibility of regenerating single pages without the need of rebuilding entire front-end application each time new content is published. 

There are two ways of implementing the app in Next.js: Page Router and a newer App Router, in both, ISR works similar with some differences in Next APIs. In the base implementation content cache is invalidated in given period of time for example every 60 seconds. For the CMS editors this approach may be annoying cause they may not see their changes immediately after the publish. To address this issue we can implement on-demand revalidation and execute it every time content is published in Optmizely CMS. Keep in mind that revalidation in Next.js invalidates the cache, the actual regeneration happens when page is accessed for the first time.

First we need to implement a Content Published event. For Optimizely PaaS CMS we can use following code which hooks into the event, then finds the pages which content did changed and calls Next.js API passing security secret and the paths to changed pages:



[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class EventsInitialization : IInitializableModule
{
    private IContentEvents _contentEvents;
    private IContentRepository _contentRepository;
    private IContentLoader _contentLoader;
    private ISiteDefinitionResolver _sideDefinitionResolver;
    private IConfiguration _configuration;
    private IHttpClientFactory _httpClientFactory;

    public void Initialize(InitializationEngine context)
    {
        _contentEvents ??= ServiceLocator.Current.GetInstance();
        _contentRepository ??= ServiceLocator.Current.GetInstance();
        _contentLoader ??= ServiceLocator.Current.GetInstance();
        _sideDefinitionResolver ??= ServiceLocator.Current.GetInstance();
        _configuration ??= ServiceLocator.Current.GetInstance();
        _httpClientFactory ??= ServiceLocator.Current.GetService();
        _contentEvents.PublishedContent += ContentEvents_PublishedContent;
    }

    public void Uninitialize(InitializationEngine context)
    {
        _contentEvents ??= ServiceLocator.Current.GetInstance();
        _contentEvents.PublishedContent -= ContentEvents_PublishedContent;
    }

    private void ContentEvents_PublishedContent(object sender, ContentEventArgs e)
    {
        if (sender == null || e == null) return;

        var pages = PagesToRevalidate(e.Content);
        foreach (var page in pages)
        {
            RevalidatePages(page);
        }
    }

    protected List<BasePageData> PagesToRevalidate(IContent content)
    {
        // TODO this is simplified method to find pages referenced by the modified content. There may be more cases like deleting page, change page url and updating references, updating settings items, etc.
        if (content is BasePageData page)
        {
            return new List<BasePageData> { page };
        }

        return GetRefPages(content);
    }

    protected List<BasePageData> GetRefPages(IContent content)
    {
        var pages = new List<BasePageData>();
        if (content is BaseBlockData)
        {
            var references = _contentRepository.GetReferencesToContent(content.ContentLink, true).ToList();
            foreach (var reference in references)
            {
                var refContent = _contentLoader.Get(reference.OwnerID);
                if (refContent != null && refContent is BasePageData pageRef)
                {
                    if (!pages.Any(p => p.ContentLink.ID == refContent.ContentLink.ID))
                    {
                        pages.Add(pageRef);
                    }
                }
                else if (refContent is BaseBlockData)
                {
                    var refPages = GetRefPages(refContent);
                    foreach (var page in refPages)
                    {
                        if (!pages.Any(p => p.ContentLink.ID == page.ContentLink.ID))
                        {
                            pages.Add(page);
                        }
                    }
                }
            }
        }
        return pages;
    }

    protected void RevalidatePages(BasePageData page)
    {
        var langUrls = new List<string>();
        //TODO We take all languages, assuming pages have language fallback and changed content may be culture invariant. 
        foreach(var language in page.ExistingLanguages)
        {
            var url = UrlResolver.Current.GetUrl(page.ContentLink, language.Name);
            if (url != null)
            {
                langUrls.Add(url);
            }
        }

        var site = _sideDefinitionResolver.GetByContent(page.ContentLink, true);
        if (site == null || langUrls.Count == 0)
        {
            return;
        }

        var secretKey = _configuration["NextRevalidate:Secret"];
        var frontUrl = $"https://my-front-end-app.com/api/revalidate?secret={secretKey}";
        GetWebResponseAsync(frontUrl, langUrls);
    }

    protected async void GetWebResponseAsync(string apiUrl, List<string> urls)
    {
        HttpClient httpClient = _httpClientFactory.CreateClient();
        HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, apiUrl);
        httpRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        httpRequestMessage.Content = new StringContent(JsonConvert.SerializeObject(urls), Encoding.UTF8, "application/json");

        using (var reader = new StreamReader(await (await httpClient.SendAsync(httpRequestMessage).ConfigureAwait(continueOnCapturedContext: false)).Content.ReadAsStreamAsync(), Encoding.UTF8))
        {
            var resultText = await reader.ReadToEndAsync();
            Log.Information("Revalidate API result:", resultText);
        }
    }
}


Now we can implement "revalidate" API route in our Next.js application. This example uses Page Router, for the App Router adjust API function and use revalidatePath('/path') instead of res.revalidate('/path') The implementation reads from query parameters the secret (and compare it with value from env variable) and the path to revalidate ISR. 


import type { NextApiRequest, NextApiResponse } from 'next';
 
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) { 
  try {
    if (req.query.secret !== process.env.NEXTAUTH_SECRET) {
        return res.status(401).json({ message: 'Invalid token' });
    }

    if (!req.body){
      return;
    }

    var results: string[] = await Promise.all(req.body.map(async (url: string): Promise => {
      await res.revalidate(url);
      return url;
    }));

    return res.json({ revalidated: results });
  } catch (err) {
    console.error(err);
    return res.status(500).send('Error revalidating');
  }
}

Run your Next application with next build and next start commands and start your CMS and publish some content, the change should be visible immediately after refreshing the page.

Mar 25, 2025

Comments

Soren S
Soren S Mar 27, 2025 01:50 PM

Great, thanks 

Please login to comment.
Latest blogs
Promoted and Certified

What a busy week

Andy Blyth | May 1, 2026 |

Announcing new library: SettingsManager

When you run .net app, there have been a few ways to store settings. Those can be set via appSettings.json, or via Azure Portal AppService...

Quan Mai | Apr 30, 2026

From Prompting to Production: Optimizely Opal University Cohort and the Future of Agentic MarTech

Most organizations today are still playing with AI. They experiment with prompts, test ideas in isolated chats, and occasionally automate a task or...

Augusto Davalos | Apr 28, 2026

Six Compelling Reasons for Upgrading to CMS 13

Most software updates ask you to keep up. Optimizely CMS 13 asks something different — it asks whether your digital strategy is built for a world...

Muhammad Talha | Apr 28, 2026

Optimizely CMS 13 breaking changes: GetContentTypePropertyDisplayName

When upgrading from CMS 12 to 13, resolving property display names may not work as before. Here’s what changed.

Tomas Hensrud Gulla | Apr 27, 2026 |

Accelerate Optimizely DAM Adoption: Unlocking Business Value with Metadata Bulk Import

Accelerating Optimizely DAM Adoption How a Metadata-Driven Bulk Import Utility Unlocks Real Business Value Executive Summary For enterprises runnin...

Vaibhav | Apr 27, 2026