How to Create an XML Sitemap in Kentico 12 MVC
Wow, it is nearly the end of 2018 already. It is amazing that the older I get, time seems to move faster and faster. I can't believe how fast the time has went this year. In fact, time has went by so fast that I didn’t really have time to join in on the buzz of the Kentico 12.0 release that happened last month (well other than having some fun on Twitter celebrating the launch of Raptor).
In case you didn’t hear, the Kentico 12 release focuses 100% on moving the Kentico platform to an MVC first development methodology. This is great news for ASP.Net developers out there who want to move into more modern technologies and tooling. Kentico is even working on updating the platform to have support for Dot Net Core in the near future. If you are interested in more on the technology roadmap I would recommend checking out the Kentico MVC transition guide and definitely watch Michal’s video towards the bottom of the page.
Now, I could sit here all day and write about the benefits of MVC and how it is flat out a better choice for any Kentico web project these days, but I am not going to do that today. Instead I am going to focus a bit on the argument that I have heard from some Kentico developers, Kentico partners, and even some end clients against the move to MVC. I have heard the point that many of them say: “Why would we move to MVC? We have a CMS with a Portal Engine and pre-built Web parts that gives us so much built in functionality and moving to MVC means that you have to give up that functionality and do everything 100% in code”.
While this is a somewhat valid point, there is indeed more code involved in a Kentico MVC solution, I want to point out that the opposite of this statement is also true, and almost never said by that side of the argument. When you implement in MVC you are opening the door to using solutions that are part of the whole .Net community and not just Kentico specific. Developing your site in MVC gives you the ability to use things like NuGet packages that are well known, secure, and maintained over time. These packages can easily add in a ton of functionality to any ASP.Net site, just like web parts could for the Kentico Portal Engine.
To illustrate my point, I am going to use a concrete example of building out an XML Google Sitemap for a Kentico MVC based website. I think this makes sense as a comparison because yes, the Kentico Portal Engine has this feature built in to it, and the MVC starter site that ships with Kentico 12.0 does not.
Keep reading after the jump to see one option for How to Create an XML Sitemap in your Kentico MVC site.
The Setup
If you do not know what an XML Sitemap is for a website, then what are you doing reading my blog? Sorry, I’m just kidding. Honestly the XML Sitemap has been around for a long time, it is used by search engines like Google, Bing, and Yahoo to help with discovery of new content (Urls) that a site has. Basically, it is an SEO best practice to have an XML Sitemap on your website.
Today we are going to add one into the Kentico 12 MVC Dancing Goat Starter site. If you want to follow along and implement the sitemap, you will need the following:
- Kentico 12.0 installed on your machine (which means you have Visual Studio and SQL Server available)
- An instance of the Kentico 12 MVC starter site (Dancing Goat). Not sure how?
- An understanding of NuGet package management (if not check out this link)
- A decent understanding of the Kentico API
Step 1 Adding in SimpleMVCSitemap NuGet Package
I did a little bit of research last week on if building a sitemap from scratch still makes sense, the resounding answer is no. There are actually multiple NuGet packages out there for both the full .Net Framework 4.x and Dot Net Core. After looking at a few of them, I settled on the SimpleMVCSitemap package. The choice was somewhat easy based on the maturity of the package, the least amount of dependencies, and the popularity of the package.
Simply add in the package through the NuGet Package Manager on the DancingGoatMVC solution that the Kentico 12 installer should have created for you.
After installing the package, right click and Rebuild your solution in Visual Studio to make sure everything came in correctly and compiles without error.
Step 2 Create a New Controller to Handle Building the XML
Add in a new Controller or new class into the Controllers folder, call it SitemapController. And then add in the bottom 4 using statements that I show below.
using System; using System.Collections.Generic; using System.Web.Mvc; using CMS.DocumentEngine; using CMS.Helpers; using CMS.SiteProvider; using SimpleMvcSitemap; namespace DancingGoat.Controllers { public class SitemapController : Controller { public ActionResult Index() { } } }
The controller should have just one simple action to start with, Index. We will fill it in later.
Step 3 Add in the Sitemap Route
Adding Routes can be done in many ways in MVC, but the simplest way in my opinion is to keep everything in the ~/App_Start/RouteConfig.cs file. That way no hunting and pecking is needed. You can see inside of the Dancing Goat starter site this file has multiple routes defined for you including the Kentico “system” routes.
To start out with add a new route above the Kentico system routes to make sure your new route is hit fist (the order does matter).
public static void RegisterRoutes(RouteCollection routes) { var defaultCulture = CultureInfo.GetCultureInfo("en-US"); routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); // ADD THIS ONE, above Kentico().MapRoutes() - FOR NOW routes.MapRoute( name: "MySiteMap", url: "sitemap.xml", defaults: new { controller = "Sitemap", action = "Index" } ); // Map routes to Kentico HTTP handlers and features enabled in ApplicationConfig.cs // Always map the Kentico routes before adding other routes. Issues may occur if Kentico URLs are matched by a general route, for example images might not be displayed on pages routes.Kentico().MapRoutes(); // Redirect to administration site if the path is "admin" routes.MapRoute( name: "Admin", url: "admin", defaults: new { controller = "AdminRedirect", action = "Index" } );
Having the route defined now tells the solution to respond to any requests at ~/sitemap.xml. You can give the route name whatever name you want.
Now depending on if you are using local IIS or IIS Express you may need to tell the webserver how to handle the .xml file extension. For me, the first time I tried hitting http://localhost:50882/sitemap.xml I received a 404 because IIS Express doesn’t know what to do without a little help. I thought it was me defining a bad route, but it wasn’t.
To resolve any 404s you might have the easiest thing to do is to tell the pipeline to handle all requests. You can do this by adding one line into the root web.config file. Which should be familiar to Kentico developers out there. Inside of the system.webserver section update the modules node to have the following runAllManagedModulesForAllRequests attribute:
<system.webServer> <modules runAllManagedModulesForAllRequests="true"> <add name="CMSApplicationModule" preCondition="managedHandler" type="CMS.Base.ApplicationModule, CMS.Base" /> <modules>
Now test out the call and you should at least get through to your Controller action and be able to see a breakpoint hit if you want there.
Step 4 Add in the SimpleMVCSitemap Code and Render the Kentico Pages Tree Content
Now to the hardest part, the SimpleMVCSitemap does a great job of actually creating the XML you need, you need to worry about parsing out strings or XMLWriters or StringBuilders here. It does that for you. The one thing you need to do is handle getting the content from the Kentico Tree out of the Kentico API and into the Sitemap object that SimpleMVCSitemap provides for you. Luckily, I have done that for you.
The code sample below has the code you need to call the Kentico API, return all published nodes from the site into a Sitemap object, and cache the call for 4 hours. You can copy this whole sample over the top of your SitemapController.cs class.
using System; using System.Collections.Generic; using System.Web.Mvc; using CMS.DocumentEngine; using CMS.Helpers; using CMS.SiteProvider; using SimpleMvcSitemap; namespace DancingGoat.Controllers { public class SitemapController : Controller { public ActionResult Index() { List<SitemapNode> nodes = new List<SitemapNode>(); foreach(var doc in GetXMLSiteMapDocuments()) { nodes.Add(new SitemapNode(doc.NodeAliasPath) { LastModificationDate = (doc.DocumentModifiedWhen.Date) }); } return new SitemapProvider().CreateSitemap(new SitemapModel(nodes)); } private MultiDocumentQuery GetXMLSiteMapDocuments() { //Define some sitewide parameters var culture = "en-us"; var siteName = SiteContext.CurrentSiteName; //Define required parameters for data call var defaultPath = "/%"; var defaultWhere = "(DocumentShowInSiteMap = 1) AND (DocumentMenuItemHideInNavigation = 0) AND (NodeLinkedNodeID IS NULL) AND (ClassName NOT LIKE '%Section')"; var defaultOrderBy = "NodeLevel, NodeOrder, DocumentName"; var defaultColumns = "DocumentModifiedWhen, DocumentUrlPath, NodeAliasPath, NodeID, NodeParentID, NodeSiteID, NodeACLID"; //Grabbing the list of available types to potentially filter out types like containers and folders // but for now, I'm not going to worry about it var classNameList = DocumentTypeHelper.GetClassNames(TreeProvider.ALL_CLASSNAMES); Func<MultiDocumentQuery> dataLoadMethod = () => DocumentHelper.GetDocuments() .PublishedVersion(true) .Types(classNameList) .Path(defaultPath) .Culture(culture) .CombineWithDefaultCulture(true) .Where(defaultWhere) .OrderBy(defaultOrderBy) .Published(true) .Columns(defaultColumns); //Cache settings set to 4 hours, but with dependency's on if any page changes in the tree var cacheSettings = new CacheSettings(240, "data|xmlsitemap", siteName, culture) { GetCacheDependency = () => { // Creates caches dependencies. This example makes the cache clear data when any node is modified, deleted, or created. string dependencyCacheKey = $"node|{siteName}|/|childnodes"; return CacheHelper.GetCacheDependency(dependencyCacheKey); } }; return CacheHelper.Cache(dataLoadMethod, cacheSettings); } } }
If you look at the Index method of SitemapController you can see it is simple. We are leveraging the SitemapProvider object that ships with the NuGet package and rendering out any SitemapNodes that we find from the content tree.
The special sauce is in the GetXMLSiteMapDocuments custom method. We are using Kentico’s MultiDocumentQuery to return us all of the documents that are published and in the site’s main culture, while still respecting a few content settings like Show In Sitemap and Hide From Navigation.
Pro Tip: I DO NOT recommend you keep GetXMLSiteMapDocuments in the controller class file. You should have other layers of your codebase that contain methods like this to retrieve Content from the Kentico Page Tree and populate View Models elsewhere, move this method there. But For the purposes of this sample it made sense to keep it all in one screen shot / code sample.
Another hugely important part of this sample is the caching that is being done. Think about it, you do not want to run a live SQL call to query every page of your website, what if your site had 30,000 pages or more? This would be very intensive. To prevent this, the code is cached and only run once every 4 hours. The cache also has a dependency on if new pages are added, pages are modified, or pages are deleted, that event will flush the cache.
I’m also eliminated pages in the Dancing Goat site that are just “sections” of content in the Where clause (Page types that end in a code name of Section, ex, DancingGoat.AboutUsSection. Those are not real pages with full Urls, they are just partial sections of a page. This tactic is very common in MVC. You could tweak this code to your needs very easily if you have more page types or sections of content that aren’t actual full Urls or you want different cache dependencies.
The Result / Let’s Run our MVC Site
With all the parts and pieces in place, you can now run your project locally, and browse to http://localhost:/sitemap.xml to see the results. It should look like this:
If everything works that's great. Then I would go back to step 3 and move the sitemap route down below the Kentico MapRoutes call right below the AdminRedirect route and then make sure it still works after that. It should.
Conclusion
There you have it, this solution should take all of about 30 mins to throw together on your MVC project. It actually took longer to do the research on different NuGet packages than to write the code to call the Kentico API.
Now the counter point could be, but Brian, this should only be about 5 mins of adding the Google XML Sitemap web part onto the Design tab of my Kentico site. I still don’t see the point. My rebuttal to that statement would be, hey, have you looked at the output of the built-in web part? Closely? See any issues?
Chances are that the out-of-the-box configuration only shows part of your site pages and page types, and not the whole thing. You would still need to go configure that web part to include custom page types that you add to your site, and even then, I have seen that web part ignore grandchildren pages because the “middle” child page type was not included. Basically, I have seen it create large headaches. I’ve even blogged about Rocking the Google Site Map feature of Kentico before.
I’d almost guarantee you are going to spend the same or more time on the Design tab as you do in MVC. And the next time a developer adds a new Page Type, your config is already out of date and your sitemap not updated. Don’t get me wrong, the web part is a good solution, I have used it a hundred times, but it is not a perfect “no touch” solution by any means. It never was intended to be either. I just feel that the MVC solution is a bit more bullet proof by always looking at published pages with full Url endpoints. This MVC solution could also be taken to the next level very easily by any ASP.Net developer, and not just a Kentico developer.
I hope you have seen how easy it can be to add in features to a Kentico 12 MVC site like a Google XML Sitemap. I would encourage any and all Kentico developers and partners to make the move to MVC. And you don’t just have to take my word for it. Check out Kentico’s official post on which development model should you choose for a Kentico EMS site.