Skip to main content

How to serve dynamic content from Sitecore

While the goal of Uniform is to enable you to have your entire site decoupled from Sitecore CD instances, there are cases where you will need to retain CD instances. This may be temporary (e.g. you are incrementally adopting a Jamstack architecture) or more long-term (e.g. sections of your site are too complex to modify and must be left in place until the next site redesign project).

This article covers the things you need to understand in order to be able to effectively serve dynamic content from Sitecore when your site uses a Jamstack architecture.

Understanding server-side logic & Jamstack#

Sitecore sites often include logic that must run on the Sitecore CD instance. Examples include conditional logic based on browser cookies or user session data. This logic is executed during the page rendering process. With a traditional Sitecore architecture, this process runs every time a visitor requests a page.

When you move to a Jamstack architecture, the page rendering process runs at publish time. Conditional logic is still executed, but since it runs at publish time, it does not have access to any runtime context. At runtime, the page is delivered by the CDN. This means that any dynamic functionality (such as search and personalization) must be reconsidered.

note

Uniform offers a way to migrate some conditional logic so it can run on the edge. In general, if you can convert your server-side logic into if-else statements and if the logic does not involve any Sitecore APIs being called at runtime, this can be a useful approach. For details, see conditional logic.

Handling custom endpoints on your Sitecore CD instance#

It is common for Sitecore developers to add custom endpoints to their Sitecore CD instances in order to support dynamic functionality. Search functionality is often implemented in this way:

  • Visitor enters a search term in a text box
  • Browser makes a call to an endpoint on the Sitecore CD instance
  • Server-side search logic runs
  • Endpoint returns search results to the browser
  • Browser injects the search results into the page

With a Jamstack architecture, visitors do not connect to your CD instance. They connect to your CDN. However, there are several ways you can support this sort of functionality without changing your endpoint.

CDN as a proxy#

The easiest way to enable access to your endpoint is to configure your CDN to act as a proxy to the origin (i.e. Sitecore CD instance). This means the browser sends requests to the CDN, but the CDN forwards the request to the origin.

The benefit of this approach is that it allows you to focus on the other tasks involved in transitioning to a Jamstack architecture without changing API paths or API cookie handling.

The way you do this depends on your CDN.

Example (Netlify)#

If the url for your endpoint is /api, you would add the following line to your _redirects file:

/api/*    https://sitecore-cd.example.com/api:splat    200!

Dedicated subdomain#

You can move your endpoint so it is hosted on a separate subdomain (and potentially separate server infrastructure) from the origin. For example, your origin might be using the domain name example.com while your api is using api.example.com.

The benefit to this approach is you get clearer separation between your API and your website, allows them to progress in parallel and be deployed independently. Since this approach involves using different domain names, you must keep a close eye on breaking changes in your API as these can impact code running in production.

Response transformation#

Most CDNs support some sort of serverless function capability that allows you to configure simple code to run when the CDN receives a request. You can use these functions to make calls to your endpoint.

There are several benefits to this approach:

  • You can manipulate the response from the endpoint before it is returned to the client. This can be useful during transitional periods when you need to change the implementation of your endpoint without making breaking changes to the API.
  • You can communicate with other services and APIs directly, without having to go through the Sitecore CD instance. This is useful when your endpoint makes calls to external systems. This allows you to remove Sitecore as the intermediary between the endpoint and the external system.
  • Serverless functions scale automatically based on demand.

Example (Netlify & Next.js)#

First you must add the Next.js plugin to your site. Then you can add your API handler(s) to your codebase, in the folder /pages/api.

Next.js supports dynamic API routes that allow you to use a single handler to process multiple routes. For example, you can write one handler that processes /api/* routes. When it comes to supporting dynamic routes, you have two options.

Option 1. Catch-all routes

A catch-all route supports /api/*. For example, the following routes are supported:

  • /api/1
  • /api/1/2

To define a catch-all route for /api/*, your handler is defined in a file /pages/api/[...path].js. Within your handler, the route values are available in a query string parameter that matches the file name (i.e. path). For example:

export default function handler(req, res) {  const { path } = req.query;  res.end(`Path values: ${path.join(", ")}`);}

Consider a case where you have an endpoint that lets you perform a simple search. The endpoint is located at https://origin.example.com/services/search, and each term you want to search for is added to the url. For example, if you want to search for "where to go", you would use https://origin.example.com/services/search/where/to/go.

The endpoint returns JSON that looks like the following:

{    "count": 2,    "hits": [        "/places",        "/people"    ],    "query": "where to go",    "root": "https://origin.example.com",}

Since you don't want the browser to access the origin (i.e. https://origin.example.com) directly, you need to transform the search results to point to your CDN (i.e. https://cdn.example.com before those results are returned to the browser. The following handler implements this logic:

export default async function handler(req, res) {  //  //Read the path from the url  const { path } = req.query;  //  //Call the endpoint on the origin  const data = await fetch(    `https://origin.my-example.com/api/search/${path.join("/")}`  );  //  //Read the response  const json = await data.json();  //  //Transform the results  json.root = "https://cdn.my-example.com";  //  //Return the transformed results  res.status(data.status).json(json);}

This handler will return the following JSON:

{    "count": 2,    "hits": [        "/places",        "/people"    ],    "query": "where to go",    "root": "https://origin.example.com",}

Option 2. Optional catch-all routes

An optional catch-all route supports /api and /api/*. For example, the following routes are supported:

  • /api
  • /api/1
  • /api/1/2

An optional catch-all route for this example is defined in a file /pages/api/[[...path]].js.

The code for the handler is identical to the (non-optional) catch-all route. The only thing you need to change is logic is needed to handle the case when the parameter value is undefined.

Handling complex pages#

If you find sections of your site that require complex server-side logic that cannot be rebuilt without significant changes across your codebase, you may decide that it is best to keep those sections in place and focus your efforts on the parts of the site that are less complex.

In this case, it is best to configure your CDN to act as a proxy where the CDN makes the request to the origin and then returns the response to the browser.

The way you configure depends on your CDN.

Example (Netlify)#

If the url for your endpoint is /api, you would add the following line to your _redirects file:

# Proxy a single page/products/complex    https://origin.example.com/products/complex    200!
# Proxy a base route/products/*    https://origin.example.com/products/:splat    200!

Handling long build times#

Unlike a traditional Sitecore site, the pages in a Sitecore site that uses a Jamstack architecture are not generated on demand, when a visitor views the page. The pages are generated in advance, either at publish time or when the page markup or code is changed.

This process tends to be extremely fast, especially since multiple pages are built in parallel. However, there are cases where this process becomes too time consuming to be practical. Examples include an unusually large number of pages, or a large number of pages that take the Sitecore server an unusually long amount of time to render.

note

To give a sense of scale here, Uniform has worked with sites consisting of 10s of thousands of pages with build times of less than 10 minutes. The number of pages you need in order to run into a problem are an order of magnitude greater.

If you find yourself in a situation where build times are too long, you should follow a two step process to resolve this problem:

  1. Diagnose and resolve the root cause
  2. Leverage advanced features from your static-site generator (SSG)

Diagnose & resolve the root cause(s)#

  • Identify and optimize particularly slow running pages. Even a small improvement in performance can make a large difference during the Jamstack build process.

    A 0.1 second improvement in rendering time may reduce build time in a site with 20,000 pages by 30 seconds, but a site with 2 million pages will see a reduction of nearly an hour.

  • Export site content to static files and use those files during the build process. Usually the most expensive operation during the Sitecore page rendering process is reading content from the Sitecore database. Uniform can extract content from the database into static files. The build process can read content from those files. This often results in a large reduction in the amount of time it takes for the build process to complete.

Leverage advanced SSG features#

If you were unable to resolve the problem(s) causing excessive build times, the next option is to use the features of your static-site generator (SSG).

Next.js#

Next.js offers fallback and incremental static regeneration (ISR) as tools to handle sites that cannot be built in an acceptable amount of time.

note

This feature requires Next.js version 10.0.6 or greater. If you are using Netlify, you must have the Next.js plugin installed, as well.

Step 1. Convert the export script

  1. In your Next.js project, open package.json.

  2. In the export script, remove && next export. It should look something like the following:

    "export": "cross-env-shell NODE_ENV=production UNIFORM_BUILD_MODE=export \"npm run build && next export\"",
note

You still need the environment variable UNIFORM_BUILD_MODE=export because some paths will still be exported.

Step 2. Enable fallback

Fallback is enabled on getStaticPaths by changing the fallback property on the object that is returned:

  • If an object is returned and the fallback property is already is set, change the value to 'blocking'.
  • If an object is returned and the fallback property is not set, set the value to 'blocking'.

The following is an example of what the code should look like:

export async function getStaticPaths() {  // await import is essential here because getStaticPaths  // only server-side and import must only happen server   // side (on client-side the code will blow up)  const { getStaticPaths } = await import('@uniformdev/next-server');
  if (process.env.UNIFORM_BUILD_MODE !== 'ssr') {    // If we are exporting the site directly or via a publish    // specify all static paths and let nextjs handle 404    return {      paths: await getStaticPaths(),      fallback: false,      fallback: 'blocking',    };  } else {    // If we are running the site in dynamic preview then    // handle all paths and render them on the fly    return {      paths: [],      fallback: 'blocking',    };  }}

Step 3. Identify ISR exclusions

Some Sitecore paths should not be included in the static export. You must filter those out by configuring the Uniform connector for Sitecore to exclude these paths from the map.

For details on how to do this, see the Map service documentation.