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. Open the Developer area

1c8c595 Click developer on the dashboard

  1. Click Dynamic pages

437909a Click dynamic pages in the developer area

  1. Check to see that the page type exists, if not, click the Create schema button, this will open the schema editor

14b446e Click create schema to create a new dynamic page schema

  1. Input your schema into the JSON editor, click Validate, then Publish

30f2c91 Input your schema validate and 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. Check it's added to your dynamic pages

3d5bba7 Check its been added

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

  1. Click Dynamic pages on the left-hand navigation

a1d1192 Click dynamic pages on the left hand navigation

  1. Select the Star Wars movie dynamic page and then click + New page version on the right

fa0491d Select star wars move

  1. Click the blue add icon in the middle section of the page builder

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 (we'll add our Star Wars opening crawl component from the developing a data source extension article)

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:

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 the /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' 'https://<sandbox-public-url>/frontastic/page?locale=en_US&path=/movie/star-wars-episode-4/ZmlsbXM6MQ=='

This 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