Generating a list of routes for all Umbraco content
Hi all,
I am attempting to use Umbraco as a headless CMS with Nuxt/Vue, by making use of the wonderful HeadRest package.
As I intend to make lots of websites with this particular setup, I want to make sure anything I write is reasonably generic. This would be much simpler for a single site with hardcoded routes and the like, but that isn't really an option here.
Before getting into any further detail, I would like to advise that I can't use Heartcore for various reasons, including cost, being cloud based, and not being extensible. Additionally, I think I would face the same problem that I am about to outline below when using Heartcore.
Essentially, what I want to achieve is the full flexibility of Umbraco's routing, but with a Nuxt/Vue application, which is very restrictive about routing. I don't want our CMS users to be restricted in how they can lay out their content or their URLs due to an inflexibility in Vue's default router.
I have raised an issue on Nuxt's GitHub repository to see if there's anything that can be done on their end to improve compatibility. (see here: https://github.com/nuxt/nuxt.js/issues/7437)
I have also had a few conversations in Nuxt's Discord channel. In one such conversation, someone suggested copying what a Storyblok plugin for Nuxt does, which is to generate a list of all the available routes, and register them in the Vue app as the page loads. Here's the link to that code: https://github.com/wearewondrous/nuxt-storyblok-router/blob/master/lib/module.js
My question is, is it possible to generate such a list of all of Umbraco's routes in an efficient way? I want to generate a JSON array of any and all routes, optimising it as much as possible.
By routes, I mean the following:
A route to each piece of content, such as /blog, /blog/blog-post-1, /products, /product-1 and so on
A route to any custom routes added via 'MapUmbracoRoute'
At its most basic implementation, I would be happy with a 1-1 map between a piece of content and a generated route, but ideally I'd like to be smarter about it wherever possible. By that I mean, rather than generating 1000 routes for all blog posts, I'd like to generate one '/blog/:slug' route, as Umbraco's permissions indicate that only blog posts can exist beneath the blog node.
I'm also thinking that I'd need some level of support for the following scenarios:
Routing/redirecting to content that has been renamed or moved
Routing to content that has aliases
Routing to content with a different umbracoUrlName
Support for multi-root sites
Registering new routes for new content that is created after the Vue app is started
Properly handling a 404 for content that goes away after the Vue app is started
Does anyone have any input on how I could go about this? Is there anything else that I haven't thought of?
I'll provide the source code for that below. It is only a rough experiment, so it's calling the ContentTypeService many times (ideally we want to cache the whole list of document type permissions in memory on app start and fetch from that), and it doesn't consider a few cases. It might be enough for you to start with, though.
I have been stopped dead in my tracks, as it turns out that Nuxt only lets you add routes at build time, meaning that these routes can't be updated as content changes in Umbraco. For example, if you renamed the blog to 'News' then Nuxt would not be able to visit the blog/news page until it was re-built.
Ideally I would like to keep any discourse about the issue public, so as to help anyone else that come across the same problem. It will also have a nice side effect of bumping the relevant forum thread and/or GitHub issue, which will hopefully bring more attention to the topic.
RouteController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using MyProject.Models;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web.WebApi;
namespace MyProject.Controllers
{
/// <summary>
/// A controller for accessing the routes in Umbraco,
/// so that they can be registered in Nuxt.
/// </summary>
public class RouteController : UmbracoApiController
{
#region Public
#region Methods
/// <summary>
/// Compile a list of all the routes in Umbraco.
/// </summary>
/// <returns>A list of routes</returns>
public IEnumerable<RouteSummary> GetRoutes()
{
List<RouteSummary> result = new List<RouteSummary>();
try {
IPublishedContent root = Umbraco.ContentAtRoot().FirstOrDefault();
if ( root != null ) {
result.AddRange( GetRoutesForContent( root ) );
}
}
catch ( Exception ) {
// todo: log this
}
return result;
}
#endregion
#endregion
#region Private
#region Methods
/// <summary>
/// Get the routes for a given piece of content, including any descendants
/// </summary>
/// <param name="content">The content to get the routes for</param>
/// <returns>The collection of routes</returns>
private IEnumerable<RouteSummary> GetRoutesForContent( IPublishedContent content )
{
List<RouteSummary> result = new List<RouteSummary>();
string documentTypeAlias = content.ContentType.Alias;
string basePath = content.Url;
List<string> allowedTypes = GetPermissions( documentTypeAlias );
result.Add( new RouteSummary( basePath, documentTypeAlias ) );
int allowedCount = allowedTypes?.Count ?? 0;
if ( allowedCount == 1 ) {
result.Add( new RouteSummary( $"{basePath}:slug", allowedTypes?.First() ) );
// todo: edge case with recursive n-level of same type
// todo: edge case with child having different permissions
} else if ( allowedCount > 1 ) {
foreach ( IPublishedContent child in content.Children ) {
result.AddRange( GetRoutesForContent( child ) );
}
}
// todo: redirects, aliases
return result;
}
/// <summary>
/// Get a list of allowed child document types for the given content
/// </summary>
/// <param name="documentTypeAlias">The document type to check permissions for</param>
/// <returns>The list of allowed document types</returns>
private List<string> GetPermissions( string documentTypeAlias )
{
// todo: refactor to cache this and reduce calls to ContentTypeService
IContentType type = Services.ContentTypeService.Get( documentTypeAlias );
List<string> allowed = type.AllowedContentTypes.Select( x => x.Alias ).ToList();
return allowed;
}
#endregion
#endregion
}
}
RouteSummary.cs
namespace MyProject.Models
{
/// <summary>
/// A Model for collecting the basic information about
/// an Umbraco route.
/// </summary>
public class RouteSummary
{
#region Public
#region Constructors
/// <summary>
/// Construct a <see cref="RouteSummary"/>
/// </summary>
public RouteSummary( string path, string contentType )
{
Path = path;
ContentType = contentType;
}
#endregion
#region Properties
/// <summary>
/// The route path
/// </summary>
public string Path
{
get;
set;
}
/// <summary>
/// The type of content to load for this route
/// </summary>
public string ContentType
{
get;
set;
}
#endregion
#endregion
}
}
So i did some experimenting this weekend. Instead of generating a list of routes that we initialize on build I created a middleware that is called before the page is loaded, in the middleware i make a request using Axios to our API passing the requested url and get the page properties.
Then I store the data in the Vuex store so that i can access my page properties when the page is loaded. I also created a "shared content" that holds the menu items, general site settings etc.
Downside so far is that i'm not sure if your locked into only one .vue file, multiple would be nice because that would work more like a MVC pattern that we're used to.
Did any of you make any progress on this issue? Our team wants to start developing Umbraco solutions completely headless and need to pick a frontend framework. We decided to try prototyping some stuff in Vuejs/Nuxt, but were stopped by the exact issue you are describing here.
My best solution so far sounds like a simplistic version of what Cimplex is describing in their latest post - basically everything is rendered in the same "base"-component and every url is routed to that one component. It works ok-ish but does not feel like you are using Nuxt the way it is intended, which is a red flag for me.
Generating a list of routes for all Umbraco content
Hi all,
I am attempting to use Umbraco as a headless CMS with Nuxt/Vue, by making use of the wonderful HeadRest package.
As I intend to make lots of websites with this particular setup, I want to make sure anything I write is reasonably generic. This would be much simpler for a single site with hardcoded routes and the like, but that isn't really an option here.
Before getting into any further detail, I would like to advise that I can't use Heartcore for various reasons, including cost, being cloud based, and not being extensible. Additionally, I think I would face the same problem that I am about to outline below when using Heartcore.
Essentially, what I want to achieve is the full flexibility of Umbraco's routing, but with a Nuxt/Vue application, which is very restrictive about routing. I don't want our CMS users to be restricted in how they can lay out their content or their URLs due to an inflexibility in Vue's default router.
I have raised an issue on Nuxt's GitHub repository to see if there's anything that can be done on their end to improve compatibility. (see here: https://github.com/nuxt/nuxt.js/issues/7437)
I have also had a few conversations in Nuxt's Discord channel. In one such conversation, someone suggested copying what a Storyblok plugin for Nuxt does, which is to generate a list of all the available routes, and register them in the Vue app as the page loads. Here's the link to that code: https://github.com/wearewondrous/nuxt-storyblok-router/blob/master/lib/module.js
My question is, is it possible to generate such a list of all of Umbraco's routes in an efficient way? I want to generate a JSON array of any and all routes, optimising it as much as possible.
By routes, I mean the following:
At its most basic implementation, I would be happy with a 1-1 map between a piece of content and a generated route, but ideally I'd like to be smarter about it wherever possible. By that I mean, rather than generating 1000 routes for all blog posts, I'd like to generate one '/blog/:slug' route, as Umbraco's permissions indicate that only blog posts can exist beneath the blog node.
I'm also thinking that I'd need some level of support for the following scenarios:
Does anyone have any input on how I could go about this? Is there anything else that I haven't thought of?
Thanks
Sorry to bump this, but does anyone have any ideas?
Thanks
Hi Dan, I'm facing the exact same dilemma. Maybe we can team up and find a solution, please send me your contacts, [email protected]
/ H
Hi,
I made a rudimentary controller that inefficiently generates a list of content routes, returning them in an array like this:
I'll provide the source code for that below. It is only a rough experiment, so it's calling the ContentTypeService many times (ideally we want to cache the whole list of document type permissions in memory on app start and fetch from that), and it doesn't consider a few cases. It might be enough for you to start with, though.
I have been stopped dead in my tracks, as it turns out that Nuxt only lets you add routes at build time, meaning that these routes can't be updated as content changes in Umbraco. For example, if you renamed the blog to 'News' then Nuxt would not be able to visit the blog/news page until it was re-built.
Someone has left a comment on my GitHub issue showing a way to add routes at runtime, so I'll be giving that a try soon. https://github.com/nuxt/nuxt.js/issues/7437
Ideally I would like to keep any discourse about the issue public, so as to help anyone else that come across the same problem. It will also have a nice side effect of bumping the relevant forum thread and/or GitHub issue, which will hopefully bring more attention to the topic.
RouteController.cs
RouteSummary.cs
Hi again Dan, Would you mind sharing the code snippet you used when you registered the initial routes in nuxt using your API call?
Did you make a request from the nuxt.config file?
Hi,
Please see below. I borrowed heavily from this repository: https://github.com/wearewondrous/nuxt-storyblok-router
Create a Nuxt Module with these three files, and then register the module in your nuxt.config.js
Please let me know if you manage to get further with it than I did.
Thanks
logger.js
utils.js
module.js
Hi again Dan,
So i did some experimenting this weekend. Instead of generating a list of routes that we initialize on build I created a middleware that is called before the page is loaded, in the middleware i make a request using Axios to our API passing the requested url and get the page properties.
Then I store the data in the Vuex store so that i can access my page properties when the page is loaded. I also created a "shared content" that holds the menu items, general site settings etc.
Downside so far is that i'm not sure if your locked into only one .vue file, multiple would be nice because that would work more like a MVC pattern that we're used to.
// Herman
Was a while ago we discussed this, did you ever find a solution?
Hi Dan and Herman
Did any of you make any progress on this issue? Our team wants to start developing Umbraco solutions completely headless and need to pick a frontend framework. We decided to try prototyping some stuff in Vuejs/Nuxt, but were stopped by the exact issue you are describing here.
My best solution so far sounds like a simplistic version of what Cimplex is describing in their latest post - basically everything is rendered in the same "base"-component and every url is routed to that one component. It works ok-ish but does not feel like you are using Nuxt the way it is intended, which is a red flag for me.
@Jakob, please drop me an email so we can discuss this further: [email protected]
is working on a reply...