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
  271
(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
Developer Meetup - London, 24th April 2025

Next Thursday, 24th April will be Candyspace 's first Optimizely Developer Meetup, and the first one held in London this year! We've have some...

Gavin_M | Apr 14, 2025

Successful Digitalization for SMEs: How Optimizely One can Revolutionize Your Business Processes

"Achieve digital excellence with Optimizely One: Boost efficiency, delight customers, secure growth." In today's digital world, it's crucial for...

Frank Hohmeyer | Apr 11, 2025

Personalized Optimizely CMS Website Search Experiences Azure AI Search & Personalizer

In the last blog, we discussed Integrating the Optimizely CMS website with Azure AI search. Now let’s take a bit more advanced topic to serve...

Naveed Ul-Haq | Apr 10, 2025 |

Integrating Optimizely CMS with Azure AI Search – A Game-Changer for Site Search

Want to elevate your Optimizely PaaS CMS site’s search capabilities? Azure AI Search could be just the tool you need! In this blog, I’ll discuss......

Naveed Ul-Haq | Apr 9, 2025 |

Opensource release: New Package Explorer for Optimizely CMS

The import/export ".episerverdata" packages have been around as far as I can remember - and even though they might seem a bit outdated, it's still...

Allan Thraen | Apr 9, 2025 |

Shorten your cache keys, please

In a recent customer engagement, I have looked into a customer where their memory usage is abnormally high. Among other findings, I think one was n...

Quan Mai | Apr 9, 2025 |