Five New Optimizely Certifications are Here! Validate your expertise and advance your career with our latest certification exams. Click here to find out more

Tomek Juranek
Mar 25, 2025
  162
(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
Transitioning to Application Insights Connection Strings: Essential Insights for Optimizely CMS

Transitioning to Application Insights Connection Strings: Essential Insights for Optimizely CMS As part of Microsoft's ongoing modernization effort...

Stefan Johansson | Mar 27, 2025

Save The Date - London 2025 Summer Meetup

Following last years very succesful meetup in London July 2024 https://world.optimizely.com/blogs/scott-reed/dates/2024/7/optimizely-london-dev-mee...

Scott Reed | Mar 25, 2025

Getting 404 when expecting 401

A short story about the mysterious behavior of an API in headless architecture.

Damian Smutek | Mar 25, 2025 |