Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Dan Hammond 6 posts 26 karma points
    Jun 01, 2020 @ 15:18
    Dan Hammond
    0

    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?

    Thanks

  • Dan Hammond 6 posts 26 karma points
    28 days ago
    Dan Hammond
    0

    Sorry to bump this, but does anyone have any ideas?

    Thanks

  • Cimplex 108 posts 570 karma points
    22 days ago
    Cimplex
    0

    Hi Dan, I'm facing the exact same dilemma. Maybe we can team up and find a solution, please send me your contacts, info@cimplex.se

    / H

  • Dan Hammond 6 posts 26 karma points
    22 days ago
    Dan Hammond
    0

    Hi,

    I made a rudimentary controller that inefficiently generates a list of content routes, returning them in an array like this:

    [
      {
        path: "/",
        contentType: "home"
      },
      {
        path: "/blog",
        contentType: "blogArea"
      },
      {
        path: "/blog/:slug",
        contentType: "blogPost"
      }
    ]
    

    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

    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
      }
    }
    
  • Cimplex 108 posts 570 karma points
    21 days ago
    Cimplex
    0

    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?

  • Dan Hammond 6 posts 26 karma points
    21 days ago
    Dan Hammond
    0

    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

    import consola from 'consola'
    
    export default consola.withScope('nuxt:umbraco-router')
    

    utils.js

    export function addTrailingSlash (str) {
      return str.replace(/\/?(\?|#|$)/, '/$1')
    }
    

    module.js

    import axios from 'axios'
    import { addTrailingSlash } from './utils'
    import logger from './logger'
    
    export default async function Module (moduleOptions) {
      const defaultOptions = {
        accessToken: '',
        contentTypeDir: 'pages',
        disabled: false,
        exclude: [],
        useFallback: false
      }
    
      const options = {
        ...defaultOptions,
        ...moduleOptions
      }
    
      if (options.disabled) {
        logger.warn('Module Disabled')
        return
      }
    
      const pages = []
    
      console.log('loading routes...')
      const response = await axios.get('http://localhost:44369/umbraco/api/route/getroutes')
      pages.push(...response.data)
    
      const filteredRoutes = pages.filter(Boolean)
      const nomalizedDir = addTrailingSlash(options.contentTypeDir)
    
      this.extendRoutes((routes) => {
        filteredRoutes.forEach((route) => {
          const component = `${nomalizedDir}${route.ContentType}`
    
          routes.push({
            // name: route.name,
            path: route.Path,
            chunkName: component,
            component
          })
        })
    
        console.log(routes)
    
        if (options.useFallback) {
          routes.push({
            name: 'Fallback',
            path: '*',
            component: `${nomalizedDir}fallback.vue`
          })
        }
      })
    
      // Disable parsing `pages/`
      this.nuxt.hook('build:before', () => {
        this.nuxt.options.build.createRoutes = () => {
          return []
        }
      })
    }
    
  • Cimplex 108 posts 570 karma points
    1 week ago
    Cimplex
    0

    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

Please Sign in or register to post replies

Write your reply to:

Draft