Developing a dynamic page extension

Standard pages (also known as static pages) in the studio have a fixed URL.

Dynamic pages allow you to create classes of pages with dynamic URLs and load different data based on the URL or other circumstances.

The typical example for a class of dynamic pages are product detail pages: These follow a specific URL scheme and load different product data depending on the URL.

The dynamic page extension gives the full URL scheme power to you by applying the following algorithm in the API hub when resolving a specific URL path to a page:

  1. If the path matches a page folder URL, this page folder is served.
  2. The dynamic page extension is asked to resolve the path. If it can resolve the page, this page is served.
  3. An error is served.

You can create an arbitrary number of dynamic pages, but all of them are handled in a single dynamic page extension.

Prerequisites

A data source must be implemented first.
For our example, we're using the data source example from the developing a data source extension article.

1. Specify the dynamic page type


Each class of dynamic pages needs to be announced to studio to make it configurable for studio users. For this, you need to specify a schema. For example:

{
"dynamicPageType": "example/star-wars-movie-page",
"name": "Star wars movie",
"category": "",
"icon": "stars",
"dataSourceType": "example/star-wars-movie",
"isMultiple": true
}

You can store the schema.json file wherever you like, because it's only required in the studio and not by the code.

The dynamicPageType uniquely identifies the class of pages and later connects data from the studio to the executed code. name, category, and icon are for illustrative/documentary purposes in the studio.

The dataSourceType determines the main data source on such a page. In this example, the data source type is example/star-wars-movie, which we used in the developing a data source extension article. Every dynamic page in the class of example/star-wars-movie-pages will contain a data source of this type which holds the data that belongs to the page.

The studio will indicate that this data source is automatically available and Frontend components can use it even though it wasn't explicitly configured to exist.

There's no data source just for a dynamic page

Beware: You can't define a data source purely for use in dynamic pages. You always also need to implement the correct data source extension for it. The reason is that once a data source is known to the studio, it can be placed on any page folder, even on non-dynamic pages.

The flag isMultiple determines if there are many pages in this class of dynamic page (known as a rule in the studio or if there can only be a single page. See the using dynamic pages in the studio article for more information.

2. Create the dynamic page in studio


The studio first needs to know that the class of dynamic pages actually exists. To do this, the schema needs to be created. When you open the studio, make sure you're in the Production environment, then follow the steps below:

  1. From the studio homepage, or the from the left menu, go to Developer > Dynamic pages.
  2. Check if the page type exists, if not, click the Create schema button to open the schema editor.
  3. Input your schema into the JSON editor, click Validate, then Publish.

If you input the dynamicPageType directly into the JSON editor, you don't need to add it in the required input box.

You can only access the dynamic page schema area in the Production environment, but don't worry this dynamic page won't be available to the customers until the dynamic-page-handler is updated to handle this particular path.

  1. Verify that the new page type has been added.

Once the class of dynamic pages is known by studio, an actual page needs to be created. To do this:

  1. From the studio homepage, or the from the left menu, click Dynamic pages.

  2. Select a dynamic page and then click + New page version. fa0491d Select star wars move

  3. Enter a name for your page version, then click Save.

  4. Click the blue add icon.

5f4daa7 Add a layout element

  1. Select the 1 layout element.

8c095d6 Select the 1 layout element

  1. Drag your Frontend component into the layout element.

0c8b7a0 Drag in the star wars component

  1. Click Save and you'll be taken back to the site builder.

944b63a Save your page version

  1. Click the more icon on your draft page version and select Make default.

fa245d4 Make your new page version default

Even though you've made the page live in the production environment, this dynamic page won't be available to the customers until the dynamic-page-handler is updated to handle this particular path, explained in the next step.

3. Implement the dynamic page logic


The dynamic page handler extension point doesn't support multiple functions, but just a single one. That allows you to implement arbitrary routing depending on your needs. For this example, a simple matching with regular expressions is used directly in the index.ts.

Executing API calls costs time and affects the performance of your website. Therefore, you should make as few calls as possible and execute them in parallel.

export default {
'dynamic-page-handler': async (
request: Request
): Promise<DynamicPageSuccessResult | null> => {
const starWarsUrlMatches = request.query.path.match(
new RegExp('/movie/([^ /]+)/([^ /]+)')
);
if (starWarsUrlMatches) {
return await axios
.post<DynamicPageSuccessResult>(
'https://swapi-graphql.netlify.app/.netlify/functions/index',
{
query:
'{film(id:"' +
starWarsUrlMatches[2] +
'") {id, title, episodeID, openingCrawl, releaseDate}}',
}
)
.then((response): DynamicPageSuccessResult => {
return {
dynamicPageType: 'example/star-wars-movie-page',
dataSourcePayload: response.data,
pageMatchingPayload: response.data,
};
});
}
return null;
},
// ...
};

The dynamic page handler receives the Request similar to an action extension. However, this Request is always directed to /frontastic/page and always contains a path query. As its return value, the dynamic page handler needs to return a DynamicPageResult, or null when the page can't be handled.

The example matches the given path against a URL schema like /movie/<slug>/<id> which is a common pattern for any kind of dynamic page. <slug> is an SEO component that puts a readable element into the URL while only the <id> is meaningful to actually resolve the underlying data.

If the URL matches the given pattern, the corresponding movie is loaded and a DynamicPageSuccessResult is returned. This result contains the class of dynamic page that was inferred by the code as dynamicPageType. The API hub uses this identifier to resolve the page layout and settings from the configuration in the studio. The dataSourcePayload is made available as a magical data source on the corresponding page (remember that a dataSourceType was defined previously in the schema).

The code used here to fetch the data for a movie is the same one as in the developing a data source extension article. In your actual code, you'd usually have a method/function encapsulating this code and call it in both places.

The pageMatchingPayload is used to provide a specific version of the data source data that's used to match studio rules (also known as FECL. In many cases, this payload is the same as the dataSourcePayload. But if the dataSourcePayload is large or rather complex, this field can be used to provide a simplified version for matching.

4. Test the dynamic pages


To test the dynamic page, a standard HTTP request to /frontastic/page is used, which receives a path that conforms to the URL schema matched by the dynamic page logic that was just implemented. For example:

curl -X 'GET' -H 'Accept: application/json' -H 'Commercetools-Frontend-Extension-Version: STUDIO_DEVELOPER_USERNAME' 'https://EXTENSION_RUNNER_HOSTNAME/frontastic/page?locale=en_US&path=/movie/star-wars-episode-4/ZmlsbXM6MQ=='

For information on the Commercetools-Frontend-Extension-Version header and the extension runner hostname, see Main development concepts.

The request returns the page payload for the dynamic page which includes the special __master data source:

{
...
"data": {
"_type": "Frontastic\\Catwalk\\NextJsBundle\\Domain\\PageViewData",
"dataSources": {
"__master": {
"data": {
"film": {
"id": "ZmlsbXM6MQ==",
"title": "A New Hope",
"episodeID": 4,
"openingCrawl": "It is a period of civil war.\r\nRebel spaceships, striking\r\nfrom a hidden base, have won\r\ntheir first victory against\r\nthe evil Galactic Empire.\r\n\r\nDuring the battle, Rebel\r\nspies managed to steal secret\r\nplans to the Empire's\r\nultimate weapon, the DEATH\r\nSTAR, an armored space\r\nstation with enough power\r\nto destroy an entire planet.\r\n\r\nPursued by the Empire's\r\nsinister agents, Princess\r\nLeia races home aboard her\r\nstarship, custodian of the\r\nstolen plans that can save her\r\npeople and restore\r\nfreedom to the galaxy....",
"releaseDate": "1977-05-25"
}
}
}
}
}
}

This data source is generated from the dataSourcePayload returned by the dynamic page code. The studio knows from the schema specification that a data source of the corresponding type is available on every dynamic page of that class.

6. Redirect to the correct dynamic page


As mentioned before, dynamic page URLs typically contain SEO parts, which aren't significant to fetch the actual information, besides an identifier. That also means that the non-significant part of a URL can change (and be changed) arbitrarily without affecting the actual page. For example, the URLs below would all resolve to the very same page content.

/movie/star-wars-episode-4/ZmlsbXM6MQ==
/movie/jar-jar-binks-for-president/ZmlsbXM6MQ==

This is bad, especially because search engines don't like duplicated content over time. The correct behavior to fix this issue is to redirect to the canonical URL. The dynamic page extension point allows you to do this by returning a DynamicPageRedirectResult instead of a DynamicPageSuccessResult:

export default {
'dynamic-page-handler': async (
request: Request
): Promise<DynamicPageSuccessResult | DynamicPageRedirectResult | null> => {
const starWarsUrlMatches = request.query.path.match(
new RegExp('/movie/([^ /]+)/([^ /]+)')
);
if (starWarsUrlMatches) {
return await loadMovieData(starWarsUrlMatches[2]).then(
(
result: MovieData | null
): DynamicPageSuccessResult | DynamicPageRedirectResult | null => {
// ...
if (request.query.path !== result._url) {
console.log(
request.query.path,
result._url,
request.query.path !== result._url
);
return {
statusCode: '301',
redirectLocation: result._url,
} as DynamicPageRedirectResult;
}
// ...
}
);
}
return null;
},
};

Running a test HTTP request with the example URL from above (mind the -i-parameter for CURL to show the response headers):

curl -i -X 'GET' -H 'Accept: application/json' 'https://swiss-toby-multi-dyn-demo.frontastic.dev/frontastic/page?locale=de_CH&path=/movie/jar-jar-binks-for-president/ZmlsbXM6MQ=='

Now results in a redirect response:

HTTP/2 301
...
location: /movie/star-wars-episode-4/ZmlsbXM6MQ==
...

Further reading