Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you use routes from a headless CMS in Nuxt JS (SSR)?

The Question

I'm trying to use the routes, as defined by the CMS, to fetch content, which is used to determine which Page to load in my our Nuxt frontend. So, how do you implement your own logic for connecting routes to content, and content to pages while maintaining all of the awesome features of a Nuxt Page?

Any help would be greatly appreciated!

The Context

Headless CMSs are on the rise and we'd like to use ours with Nuxt in Universal mode (SSR).

We are tied into using Umbraco (a free-to-host CMS), which has a flexible routing system that we can't restrict without massive user backlash.

Note: Umbraco is not headless, we're adding that functionality ourselves with the Umbraco Headrest project on GitHub

Note: Each piece of content has a field containing the name of it's content type

For example, the following content structure is valid.

.
home
├── events
|   ├── event-one
|   └── event-two
|       ├── event-special-offer
|       └── some-other-content
├── special-offer
└── other-special-offer

So, if we want to render these special offers with nuxt, we will need to use Pages/SpecialOffer.vue.

The Problem

The problem is that the default paths for these special offers would be:

  • /special-offer
  • /other-special-offer
  • /events/event-two/event-special-offer

The editors can also create custom paths, such as:

  • /holiday2020/special (which could point to event-special-offer)

The editors can also rename content, so other-special-offer could become new-special-offer. Umbraco will then return new-special-offer for both /other-special-offer and /new-special-offer.

Attempted Solutions

The following approaches were promising, but either didn't work or were only a partial success.

  1. Use middleware to forward URL requests to Umbraco and determine the component to load from the result (fail)
  2. Use a plugin to forward URL requests to Umbraco and determine the component to load from the result (fail)
  3. Map the routes from the content (partial)
  4. Use a single page that uses response data to load dynamic components (partial)

1) The middleware approach

We attempted to create an asynchronous call to the CMS's API and then use the response to determine which page to load.

middleware.js

export default function (context) {
  return new Promise((resolve, reject) => {
  /** 
   * Prepend the cms provided url's path with /content,
   * which the API knows is for CMS content
   */
    context.app.$axios.$get(`/content${context.route.path}`)
      .then((content) => {
        // Save the resulting content to vuex
        context.store.commit('umbraco/load', content)

        const { routes } = context.app.router.options

        /**
         * The content has a contentType property which is a string
         * containing the name of the component we want to load
         * 
         * This finds the corresponding route 
         */
        const wantedRoute = routes.find(x => x.name === content.contentType)

        // Sets the component to load as /Pages/[componentName].vue
        context.route.matched[0].components = {
          default: wantedRoute.component
        }

        resolve()
      })
  })

Issues

When running on the server, middleware seems to run after the route is resolved, leading to the following error:

vue.runtime.esm.js?2b0e:619 [Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.

Note: on first page visit, middleware only runs on the server side and on subsequent visits, middleware only runs on the client side.

2) The Plugin Approach

As plugins run earlier in the lifecycle, we thought we'd try the same approach there and try to call the CMS's API and determine the component to load based off of the result.

plugin.js

await context.app.router.beforeEach( async ( to, from, next ) => {
  /** 
   * Prepend the cms provided url's path with /content,
   * which the API knows is for CMS content
   */
  const content = await context.app.$axios.$get( `/content${context.route.path}` );
  
  // Save the resulting content to vuex
  context.store.commit('cms/load', content);

  /**
   * The content has a contentType property which is a string
   * containing the name of the component we want to load
   * 
   * This finds the corresponding route 
   */
  const routes = context.app.router.options.routes;
  const targetRoute = routes.find( r => r.name === content.contentType );

  // Sets the component to load as /Pages/[componentName].vue
  to.matched[0].components = {
    default: targetRoute.component
  };

  next();
})

Note: Plugins run on both the server and the client on the first page visit. On subsequent visits, they only run on the client.

Issues

Unfortunately this didn't work either as app.router.beforeEach() does not allow for asynchronous code. As such, the API request never resolved.

3) The Route Mapping Approach

Generating a list of route maps worked fine unless you changed the routes after the build. The issue here is that you can't update the routes at runtime, so renaming or adding new content in the CMS would not be mirrored in Nuxt.

4) The Dynamic Component Approach

This approach seems to be the default solution to our problem, and involves using a single Page in Nuxt that calls the API and then dynamically loads the component. Unfortunately, this approach removes the majority of Nuxt's page specific features (such as page transitions).

Losing these features is really unfortunate, as Nuxt is REALLY nice and we'd like to use it as much as possible!

Nuxt JS GitHub Issues

  • I raised the failing async issue as this bug on the nuxt.js GitHub
  • I commented on the this GitHub issue which is a feature request for the same problem but has some good dialogue around the issue
like image 296
Ryan Varley Avatar asked Jun 11 '20 11:06

Ryan Varley


1 Answers

What we have tried is use one universal page template to handle all possible routes, code can be found in: https://github.com/dpc-sdp/ripple/blob/v1.23.1/packages/ripple-nuxt-tide/lib/module.js#L208

That single page template will match * routes user asks for. However, you can define multiple templates for multiple routes rules if you like.

The communication between Nuxt app and CMS API is like below:

Nuxt: Hi CMS, user want to the data of path /about-us.

CMS: Got it. Let me check the CMS database, and find the same url. If there is one, I will send the data back to you, otherwise I will give you a 404.

That way you won't lose page transition features. In that Nuxt page template, you can then send request to CMS and get the page data. This can be done in Nuxt router middleware or page async data, now maybe fetch hook is the best place. I am not sure which is the best, we are using async data which may not is the best practice.

like image 168
Tim Yao Avatar answered Nov 25 '22 18:11

Tim Yao