Sitemap is Empty in a Local XM Cloud Development Environment
In a local XM Cloud Docker development environment, the /sitemap.xml
front-end route always returns an empty XML file.
When a headless site is created, the sitemap media library item that is scaffolded is empty and has a size of 0 bytes.
This makes it impossible to debug the \src\pages\api\sitemap.ts
API route handler and modify it to our needs.
This article dives into the sitemap regeneration process and solutions for local XM Cloud Docker development environments.
Impossible to Regenerate the Sitemap
In a hosted XM Cloud environment, this sitemap item is regenerated at the end of a publish operation of any item. An OnPublishEnd
processor checks the SXA sites sitemap settings and regenerates the sitemap if enough time has elapsed since the last regeneration.
In a local Docker development environment, we cannot publish as we do not have a web database or a valid Experience Edge target. Attempting to publish results in an exception:
#Exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
---> Sitecore.ExperienceEdge.Connector.ContentHubDelivery.Exceptions.EdgeApiException: [ExperienceEdge Publishing]: An error has occurred while publishing JobStart event 9614b999-f4dc-4fb7-ab08-75bf8a2bf429 for publishing job '638580401015070000'. Please see inner exception for more details.
---> Sitecore.Exceptions.ConfigurationException: Invalid Authority connection string. Required parameter 'url' is missing.
Thus, the sitemap is never regenerated and the media library item is never updated.
Introducing the Sitecore XM Cloud Sitemap Developer Utilities
Faced with that limitation and the need to replicate a production sitemap issue in a development environment, I found creative ways to regenerate the sitemap media library item and get valuable information.
The end result is a configurable utility page that can be deployed to a local XM Cloud CM. There are a few steps to use it:
- Ensure your local XM Cloud Docker Compose project is running and the CM is healthy and accessible.
-
Create a
generateSitemap.aspx
file in your project\docker\deploy\platform
folder with the code below:<%@ Page language="c#" %> <%@ Import Namespace="Microsoft.Extensions.DependencyInjection" %> <%@ Import Namespace="Sitecore.Abstractions" %> <%@ Import Namespace="Sitecore.Configuration" %> <%@ Import Namespace="Sitecore.Data" %> <%@ Import Namespace="Sitecore.Data.Items" %> <%@ Import Namespace="Sitecore.DependencyInjection" %> <%@ Import Namespace="Sitecore.Events" %> <%@ Import Namespace="Sitecore.Globalization" %> <%@ Import Namespace="Sitecore.Pipelines" %> <%@ Import Namespace="Sitecore.Publishing" %> <%@ Import Namespace="Sitecore.Sites" %> <%@ Import Namespace="Sitecore.Web" %> <%@ Import Namespace="Sitecore.XA.Foundation.SiteMetadata.EventHandlers" %> <%@ Import Namespace="Sitecore.XA.Foundation.SiteMetadata.Sitemap" %> <%@ Import Namespace="Sitecore.XA.Foundation.SiteMetadata.Pipelines.Sitemap.GenerateSitemapJob" %> <script runat="server"> // IMPORTANT // Change the values of those variables to match the Headless SXA site name and home item you want to generate the sitemap for. private const string SITE_NAME = "sxastarter"; private const string HOME_ITEM_ID = "{BEE17B56-2406-4935-A730-CFE90356BE2C}"; </script> <!DOCTYPE html> <html> <head> <title>Sitecore XM Cloud Sitemap Developer Utilities</title> <meta content="C#" name="CODE_LANGUAGE"> </head> <body style="font-family: sans-serif"> <h1>Sitecore XM Cloud Sitemap Developer Utilities</h1> <form runat="server"> <div> Start with this button. It calls the SitemapCacheClearer.OnPublishEnd method with a mock OnPublishEnd event. The Sitecore CacheItemClearer should create one Sitemap refresh job per SXA site. It should save the sitemaps to the media library. You can track the jobs in the <a href="/sitecore/admin/Jobs.aspx" target="_blank">jobs admin page</a>. </div> <br /> <asp:Button id="SitemapCacheClearer" Text="1. SitemapCacheClearer.OnPublishEnd()" OnClick="SitemapCacheClearer_Click" runat="server"/> <br /><br /> <div> If the above button does not generate the sitemap media library items, try these buttons in order. </div> <br /> <div> This one runs the sitemap.generateSitemapJob pipeline for the site you defined in the SITE_NAME variable. It should save the sitemap to the media library. </div> <asp:Button id="GenerateSitemapJob" Text="2. sitemap.generateSitemapJob pipeline" OnClick="GenerateSitemapJob_Click" runat="server"/> <br /><br /> <div> This one runs only the GenerateSitemap processor of the sitemap.generateSitemapJob pipeline for the site you defined in the SITE_NAME variable. It does not save the sitemap to the media library. </div> <asp:Button id="GenerateSitemap" Text="3. GenerateSitemap.Process()" OnClick="GenerateSitemap_Click" runat="server"/> <br /><br /> <div> This one runs the ItemCrawler and lists the page items it found for the HOME_ITEM_ID variable you defined. It does not save the sitemap to the media library. </div> <asp:Button id="ItemCrawler" Text="4. ItemCrawler.GetItems()" OnClick="ItemCrawler_Click" runat="server"/> </form> <br /> <h2>Output</h2> <code> <%=Output %> </code> </body> </html> <script runat="server"> private string output = "None yet. Please click one of the buttons."; void SitemapCacheClearer_Click(Object sender, EventArgs e) { output = GetTime() + " Starting SitemapCacheClearer.OnPublishEnd()\n\n"; SitemapCacheClearer clearer = new SitemapCacheClearer(); Database master = Factory.GetDatabase("master"); List<string> targets = new List<string>(new string[] { "Edge" }); PublishOptions options = new PublishOptions(master, master, PublishMode.SingleItem, Language.EnglishLanguage, DateTime.Now, targets); Publisher publisher = new Publisher(options); SitecoreEventArgs args = new SitecoreEventArgs("OnPublishEnd", new object[] { publisher }, new EventResult()); clearer.OnPublishEnd(null, args); output += "Cancelled?: " + args.Result.Cancel.ToString() + "\n"; if (args.Result.HasMessages) { output += "Messages:\n"; foreach (string message in args.Result.Messages) { output += "- " + message + "\n"; } } if (args.Result.HasReturnValues) { output += "Return Values:\n"; foreach (var returnValue in args.Result.ReturnValues) { output += "- " + returnValue.ToString() + "\n"; } } output += "\n" + GetTime() + " SitemapCacheClearer.OnPublishEnd() done. The Sitemap should have been saved to the media library."; } void GenerateSitemapJob_Click(Object sender, EventArgs e) { output = GetTime() + " Starting the sitemap.generateSitemapJob pipeline.\n\n"; SiteContext siteContext = SiteContextFactory.GetSiteContext(SITE_NAME); GenerateSitemapJobArgs args = new GenerateSitemapJobArgs(); args.SiteContext = siteContext; BaseCorePipelineManager pipelineManager = ServiceLocator.ServiceProvider.GetService<BaseCorePipelineManager>(); pipelineManager.Run("sitemap.generateSitemapJob", (PipelineArgs)args); var messages = args.GetMessages(); output += "Aborted?: " + args.Aborted.ToString() + "\n"; output += "Suspended?: " + args.Suspended.ToString() + "\n"; if (messages.Length > 0) { output += "Messages:\n"; foreach (PipelineMessage message in messages) { output += "- " + message.Text + "\n"; } } output += "\nGenerated Sitemap:\n"; foreach (string item in args.SitemapContent.Values) { output += item + "\n"; } if (args.Aborted || args.Suspended) { output += "\nThe pipeline was aborted or suspended. The Sitemap might not have been saved to the media library.\n"; } else { output += "\nThe Sitemap should have been saved to the media library.\n"; } output += "\n" + GetTime() + " sitemap.generateSitemapJob pipeline done."; } void GenerateSitemap_Click(Object sender, EventArgs e) { output = GetTime() + " Starting GenerateSitemap.Process()\n\n"; SiteContext siteContext = Sitecore.Sites.SiteContextFactory.GetSiteContext(SITE_NAME); GenerateSitemapJobArgs args = new GenerateSitemapJobArgs(); args.SiteContext = siteContext; GenerateSitemap processor = new GenerateSitemap(); processor.Process(args); output += "Generated Sitemap (Not saved to the media library):\n"; foreach (string item in args.SitemapContent.Values) { output += item + "\n"; } output += "\n" + GetTime() + " GenerateSitemap.Process() done."; } void ItemCrawler_Click(Object sender, EventArgs e) { output = GetTime() + " Starting ItemCrawler.GetItems()\n\n"; Database master = Factory.GetDatabase("master"); Item homeItem = master.GetItem(HOME_ITEM_ID); ItemCrawler crawler = new ItemCrawler(); IList<Item> items = crawler.GetItems(homeItem); output += "Pages returned by ItemCrawler.GetItems():\n"; foreach (Item item in items) { output += "- " + item.Name + "\n"; } output += "\n" + GetTime() + " ItemCrawler.GetItems() done."; } string GetTime() { return DateTime.Now.ToLongTimeString(); } private string Output { get { return Server.HtmlEncode(output).Replace(" ", " ").Replace("\n", "<br />"); } } </script>
-
The first
<script>
block contains 2 variables you must set to the site name and site home item ID you want the sitemap to be generated for. Ensure you set those correctly.<script runat="server"> // IMPORTANT // Change the values of those variables to match the Headless SXA site name and home item you want to generate the sitemap for. private const string SITE_NAME = "sxastarter"; private const string HOME_ITEM_ID = "{BEE17B56-2406-4935-A730-CFE90356BE2C}"; </script>
-
Save the file. The container entrypoint script will copy the saved file to the CM container
wwwroot
folder. -
In your browser, load the https://xmcloudcm.localhost/generateSitemap.aspx page and follow the instructions.
-
Reload your sitemap route and enjoy a regenerated sitemap!
Tips When the Sitemap Does Not Generate
- Ensure you edited the
SITE_NAME
andHOME_ITEM_ID
variables values in thegenerateSitemap.aspx
file and you saved your changes. - Check the siteโs sitemap settings (
/sitecore/content/YourSiteCollection/YourSite/Settings/Sitemap
)- Crawler: The value should be either
itemCrawler
orindexCrawler
- Those crawlers are defined the Sitecore config under the
<sitemapItemCrawler>
node.
- Those crawlers are defined the Sitecore config under the
- Cache
- Refresh threshold: Set it to 0 to force a regeneration every time.
- Cache Type: You can play with the โStored in cacheโ and โStored in fileโ options.
- Both should update the sitemap media library item.
- Cache expiration: Set it to 0 to force a regeneration every time.
- Media items
- Generate sitemap media items: Ensure it is checked.
- Crawler: The value should be either
- Delete your site sitemap media library item (
/sitecore/media library/Project/YourSiteCollection/YourSite/Sitemaps/YourSite/sitemap
).- Sitecore will inform you that there are links to that element. Chose to remove the links.
- The sitemap media library item will be recreated and relinked correctly the next time it will be generated.
Happy Sitecoring!