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 toevent-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.
- Use middleware to forward URL requests to Umbraco and determine the component to load from the result (fail)
- Use a plugin to forward URL requests to Umbraco and determine the component to load from the result (fail)
- Map the routes from the content (partial)
- 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