Developing a dynamic page extension

Standard pages (also known as static pages) in the Frontastic studioFrontastic studio - The interface you use to manage, build, and edit all areas of your project and commerce sites. Previously known as `backstage`. have a fixed URL.

Dynamic pages allow you to create classes of pages that have 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.

With the dynamic page extension, Frontastic gives the full URL scheme power to you by applying the following algorithm in the Frontastic API hubAPI hub - The API hub combines our code, your code, and APIs to create the backend of a commerce site. Any backend development or extensions are done here. Known as `Catwalk` in code. 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

1. Specify the dynamic page type

Each class of dynamic pagesdynamic pages - A type of page that creates the default layout of pages that use the same structure but with different data, for example, a Product Details Page, wishlist, cart, search, and so on. Previously known as `master pages`. needs to be announced to Frontastic studioFrontastic studio - The interface you use to manage, build, and edit all areas of your project and commerce sites. Previously known as `backstage`. to make it configurable for Frontastic 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
}

The dynamicPageType uniquely identifies the class of pages and later connects data from Frontastic studio to the executed code. name, category, and icon are for illustrative/documentary purposes in the Frontastic studioFrontastic studio - The interface you use to manage, build, and edit all areas of your project and commerce sites. Previously known as `backstage`..

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 Frontastic studioFrontastic studio - The interface you use to manage, build, and edit all areas of your project and commerce sites. Previously known as `backstage`. will indicate that this data sourcedata source - The data provided from an API to display products, content, or anything from an integration or extension. A data source filter is created and can be selected for a Frontastic component that will display the data source filter on your commerce site. is automatically available and Frontastic componentsFrontastic components - A customizable building block that's used together with other components to create a commerce site. Known as `tastic` for short in code. 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 Frontastic studio, it can be placed on any (non-dynamic) page folder, too.

The flag isMultiple determines if there are many pages in this class of dynamic pagedynamic page - A type of page that creates the default layout of pages that use the same structure but with different data, for example, a Product Details Page, wishlist, cart, search, and so on. Previously known as `master page`. (known as a rule in the Frontastic studioFrontastic studio - The interface you use to manage, build, and edit all areas of your project and commerce sites. Previously known as `backstage`. or if there can only be a single page. See the using dynamic pages in the Frontastic studio article for more information.

2. Create the dynamic page in Frontastic studio

The Frontastic studioFrontastic studio - The interface you use to manage, build, and edit all areas of your project and commerce sites. Previously known as `backstage`. first needs to know that the class of dynamic pages actually exists. To do this, the schema needs to be uploaded. When you open the Frontastic studio, make sure you're in the Production environment, then follow the steps below:

  1. Open the Developer area
  1. Click Dynamic pages
  1. Check to see that the page type exists, if not, click the blue add icon and select your dynamic page schema

❗️

The schema needs to be uploaded in Production, otherwise, the dynamic pages can't be created later.

  1. Check it's added to your dynamic pages

Once the class of dynamic pages is known by Frontastic studioFrontastic studio - The interface you use to manage, build, and edit all areas of your project and commerce sites. Previously known as `backstage`., an actual page needs to be created. To do this:

  1. 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
  1. Click the blue add icon in the middle section of the page builder
  1. Select the 1 layout element
  1. Drag your Frontastic component into the layout element (we'll add our Star Wars opening crawl component from the developing a data source extension article)
  1. Click Save, you'll be taken back to the site builder
  1. Click the more icon on your draft page version and select Make default

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 Frontastic API hub uses this identifier to resolve the page layout and settings from the configuration in the Frontastic 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 Frontastic studio rules (also known as FECLFECL - Stands for Frontastic Entity Criterion Language. It's a way to create specific pages that will be shown to customers if that page meets the criteria given.. 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://<projectname><sandbox-host>/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 Frontastic studioFrontastic studio - The interface you use to manage, build, and edit all areas of your project and commerce sites. Previously known as `backstage`. knows from the schema specification that a data source of the corresponding type is available on every dynamic page of that class.

5. Link to dynamic pages

Frontastic doesn't know anything about the URL structure you implement in the dynamic page handler extension. So, our framework can't generate links for the corresponding pages. This needs to be part of your own code. There are generally 2 ways of how linking can be handled:

  1. The frontend code takes care of links directly
  2. The backend provides URLs for linking to the frontend

Number 1 is simple and straightforward if you only use entirely custom code. In that case, you can create a client-side function that generates a proper URL from given data and use that.

Number 2 leaves control over URL paths to the backend. This is more central and suits better as page folder URLs are also generated from the backend. So, the Frontastic starters use this way, and it's presented here.

By convention, the backend stores the path to link to a dynamic page of an entity in the special property _url. To ensure every representation of the entity, it makes sense to extract the code into a dedicated function and re-use this function everywhere the entity is sourced:

interface MovieData {
  data: {
    film: {
      id: string;
      title: string;
      episodeID: number;
      openingCrawl: string;
      releaseDate: string;
    }
  };
  _url: string;
}

const loadMovieData = async (movieId: string): Promise<MovieData|null> => {
  return await axios.post<MovieData|null>(
    'https://swapi-graphql.netlify.app/.netlify/functions/index',
    {
      query: '{film(id:"' + movieId + '") {id, title, episodeID, openingCrawl, releaseDate}}',
    }
  ).then((response): MovieData => {
    console.log(response.data)
    return {
      ...response.data,
      _url: '/movie/star-wars-episode-' + response.data.data.film.episodeID + '/' + response.data.data.film.id
    } as MovieData;
  }).catch((reason) => {
    return null;
  });
}

The code already contains the generated _url parameter. The very same function needs to be used when fetching movie data to be displayed using a data source anywhere else (see the developing a data source extension article). Or, at least, the correct _url parameter needs to be generated when loading data about the same entity in different ways.

📘

In case internationalized URLs are required, the convention is to use the property _urls which holds a hash-map mapping locale strings to URL paths. For example: { _urls: { 'en_US': '/p/foo/23', 'de_DE': '/p/bar/23' }.

The updated dynamic page extension using this function now looks like this:

return default {
  'dynamic-page-handler': async (request: Request): Promise<DynamicPageSuccessResult | null> => {
    const starWarsUrlMatches = request.query.path.match(new RegExp('/movie/([^ /]+)/([^ /]+)'));

    if (starWarsUrlMatches) {
      return await loadMovieData(starWarsUrlMatches[2]).then((result: MovieData|null) : DynamicPageSuccessResult|null => {
        if (result === null) {
          return null
        }
        return {
          dynamicPageType: 'example/star-wars-movie-page',
          dataSourcePayload: result,
          pageMatchingPayload: result,
        } as DynamicPageSuccessResult;
      });
    }

    return null;
  },
};

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


Did this page help you?