Tomek Juranek
PaaS CMS Developer Certification
Mar 25, 2025
visibility 3159
star star star star star
(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 

error Please login to comment.
Latest blogs
Surviving a Dirty Feed. Part 3: Error Handling and Event Batching

Committing a 250,000-row Optimizely catalog import row by row, returning a per-row failure report, and coalescing catalog events into one per batch.

Stanisław Szołkowski | Oct 16, 2026 |

Building UI Extensions for Optimizely SaaS CMS

Introduction Developers have been extending the functionality of Optimizely PaaS CMS for years using addons built in C# and deployed as NuGet...

Mark Stott | Oct 15, 2026

Cheapskate AI Translator: Because Translation Should Not Require a Second Mortgage

There is a particular kind of software project that begins with a detailed architecture document, a product roadmap, and a carefully assembled...

Linh Doan Cuu | Oct 15, 2026

Computed Fields in CMS 13 Content Contracts with Optimizely Graph

Content Contracts in Optimizely CMS 13 allow different content types to share a common structure when querying Optimizely Graph. But what if the...

Mardy | Oct 14, 2026