In DotNetNuke Core 5.3 there was some work done around the Core search engine sitemap provider that was referred to as “The sitemap now allows module admins to plugin sitemap logic for individual modules” in the release notes. When I first heard about this in a Core Team meeting, it was explained that modules that have many pieces of content on a single page (Ex. Forum, Blog, Articles, etc.) can be picked up by the search engine sitemap provider. Having just finished my recent blog series on Taxonomy, which also focuses on multiple content items within a single module, I figured this was the next thing I should cover. If you haven’t read my blog series on Taxonomy some of the terms here may not make sense (so take a gander if you haven’t already although it may not be necessary to follow this). One thing worth mentioning before we get started, I know there are third party search engine sitemap providers out there but I am unsure of how this implementation will work with them.
Basic Flow
Before diving into code, lets first take a look at how this works. When someone (or something) calls your DotNetNuke sitemap page (http://www.domain.com/Sitemap.aspx) the request is intercepted by an HttpHandler (this is set in your web.config). This handler will then run its ProcessRequest method. This method will look to see if a sitemap.xml file exists, and if not, will call BuildSiteMap to create one. BuildSiteMap is part of the Core Library project and basically what it does is the following:
- Checks cache settings configured via the Sitemap module (located under Admin –> Search Engine SiteMap in most installs)
- Loops through all Sitemap providers one by one to build a collection of URLs (providers are stored in web.config, visible in same sitemap module)
- Creates the sitemap.xml file (which is stored in Portals/[PORTALID]/Sitemap/ folder)
Something worth mentioning here is that there is a 50,000 page limit per sitemap file (the forum on this site alone would go above that limit) and that multiple files will need to be created if you go above this limit. However, the core will handle this for you and create multiple files if necessary. With the basic flow covered, lets dive into what we need to create one for our own module.
Module Additions
To get started, you should create a new class for your module project. What you call this class and where you place it in your module is completely up to you but my example is a class named Sitemap and it is stored in MyModule\Components\Providers\Sitemap.cs. In my newly created Sitemap class the first thing I need to do is inherit from the SitemapProvider class of the core (located under the DotNetNuke.Services.Sitemap namespace). In doing so, this is going to inform me that I must override a function named GetUrls if I attempt to compile. Next, since we want to be able to compile, we need to override this GetUrls function which returns a collection of SitemapURL.
At this point, we have to decide what should be in our modules sitemap. In some modules, this is simple. For example, a blog module will need a URL entry in our sitemap for every published blog post. In something like a forum, you have more options and it may lead to some confusion. Should it be the list of forums or should it be the list of threads? While you are free to do whatever you feel is best for your situation, I suggest creating URLs for things you equate to Content Items (discussed in Taxonomy series). In the case of a forum, I would have a Content Item for every thread (thus allowing tags to be assigned per thread) and therefore I would want a sitemap URL for each thread. Once we understand what we want in our sitemap, we need to create logic that communicates with the data store to retrieve our collection of content items on a per portal basis because the sitemap is portal wide. Now, lets take a look at my GetUrls function:
1: using DotNetNuke.Entities.Portals;
2: using MyModule.Components.Entities;
3: using System.Collections.Generic;
4: using MyModule.Components.Controllers;
5: using DotNetNuke.Services.Sitemap;
6: using System.Linq;
7:
8: namespace MyCompany.MyModule.Components.Providers
9: {
10:
11: public class Sitemap : SitemapProvider
12: {
13:
14: #region Public Methods
15:
16: public override List GetUrls(int portalID, PortalSettings ps, string version) {
17: var cntentry = new MyModuleController();
18: var colEntries = cntentry.GetPublishedPortalEntries(portalID);
19:
20: return colEntries.Select(GetBlogUrl).ToList();
21: }
22:
23: #endregion
24:
25: }
26: }
In lines 17-18 I communicate with the data store to retrieve all items that are published per portal, which are all the items I will want listed in the site’s sitemap. In line 20 I use some Linq logic to loop through the collection of content items and for each item I call my own private method shown below, which in the end results in a SitemapUrl list usable for sitemap generation.
1: private static SitemapUrl GetBlogUrl(EntryInfo objEntry) {
2: var pageUrl = new SitemapUrl
3: {
4: Url =
5: DotNetNuke.Common.Globals.NavigateURL(objEntry.TabID, "ViewEntry", "mid=" + objEntry.ModuleId,
6: "EntryID=" + objEntry.EntryId),
7: Priority = (float) 0.5,
8: LastModified = objEntry.LastModifiedOnDate,
9: ChangeFrequency = SitemapChangeFrequency.Daily
10: };
11:
12: return pageUrl;
13: }
In line 4 I set the URL property of a new SitemapUrl object. What this URL is will vary from module to module, just keep in mind that you will likely need TabID, ModuleID and whatever your identifier is (in my example above, it is EntryID). If your module is associated with Content Items, you will have ModuleID and TabID available to you via the relation to the ContentItems table even if you didn’t store either in your modules table (therefore I recommend doing a join to that table when retrieving, just like my previous taxonomy examples). In line 7 we set the priority, which I left here at the default value of .5. What this value should be varies and I will leave it up to you to decide. In line 8 we set the last time the content item was updated and finally in line 9 we tell it what the change frequency is (again, I will leave this up to you to decide). That is all we need as far as code goes, the only thing left for us is to add an entry to our web.config so the module provider is available. Towards the bottom of our web.config file, where the coreSitemapProvider is located, we need to add a line to make our new provider visible. This will look something like this:
1: <add name="MyModuleSitemapProvider" type="MyCompany.MyModule.Components.Providers.Sitemap,
2: MyCompany.MyModule" providerPath="~\DesktopModules\MyCompany\MyModule\Components\Providers\" />
Once you have done the above (compiled your module and added the web.config entry) you can start testing to see your changes. Simple go to the Admin –> Search Engine SiteMap tab for a portal that contains your module populated with content (locally) and clear the cache for the sitemap module (you may need to completely clear the site cache). At this point, you should be able to see your new provider name located in the data grid within the module. Once you see your provider, you should click the Sitemap URL link (also located in the module) to validate your module is exposing its content to the sitemap. If you are not seeing anything, you should debug to verify your provider is getting called and locate the problem by stepping through it. You should also verify that the URLs that are displayed are taking you to valid locations (and where you intended them to take you). Once all that is working, your development is done.
NOTE: Although I don’t want to cover it in great detail here, you can use a “Config” component section in the dnn manifest file (version 5.0) to automate this during install. In doing so, please be very careful otherwise you can easily bring down any site that attempts to install the module and the only way to correct it will be direct web.config access (via RDP to web server or FTP to sites root directory).
Well, that is it. Hopefully you can start taking advantage of this somewhat recent change to the core.