Is there an easy way to get the full list of all node URLs for a V17 install?

Is there a table in the DB that can be queried or some other easy way to get a listing of all the URLs for a site in Umb 17? I would be looking to generate a sitemap for both single and muti-site installs.

For building a sitemap, you should not access the database. Each time a page changes you would need to regenerate or update the sitemap and access the database again.

You’re much better of to use the published data (IPublishedContent) for multiple reasons:

  • Performance: the published data is cached
  • You only want published pages in your sitemap anyway

You can then cache the sitemap and invalidate the cache on an update.

Hi @Eaglef90

There’s examples for creating a sitemap here which should give you a good starting point:

Justin

The site map will be generated on the fly when requested. I thought of the DB cause if there are thousands of nodes reading each node individually to generate the map would be very taxing. Using cached data is also bad for large sites as there is no garnetee the cache has all URLs in it or is even built at the time.

Thanks but this is being done at the code level not in the back office. This is a feature going to a larger package being developed.

The database does not contain any URLs. URLs are only available when publishing. You can probably infer them, but that’s even worse for performance.

You really should use the cache. Yes, there is no garantee that every node is in the cache yet, but they will be after one single run. Also I really think you should cache or store the sitemap result.

Hi @Eaglef90

You have to use the published content cache to get the URLs, the URLs are not stored in the database as they are generated dynamically. The sitemap code sample I shared can be adapted, although I don’t know what you mean by generating it in the back office? It is C# code, so regardless of where it goes, it can be in the view or a service class to return the data you need that you can consume.

You can cache this yourself, so once generated it gets cached for a duration - just remember to clear the cache when pages get published or unpublished.

Maybe if you can provide a bit more detail about what you are trying to do we can guide you better?

Justin

The link you gave me was a tutorial on how to build a content node for a site map, that is what I mean by in the back office. It also appears to navigate to the root of the node and find every decendant and build a list of the URLs by reading into them. I was hoping for a more effance way, expecially for large sites that have hundreds of thousands of nodes.

I don’t know much about caching data in Umbraco, or in .net generally, and I would imagin for smaller sites digging though the cache would be fine but if a site has 100k+ nodes in it I don’t see that being better or even being able to actually all be cached. And while I can cache the sitemap for later hitting, invalidating on a publish/unpublish I have no problem with that but I can’t garentee I can write an actual .xml file to the envrironment of the user of the package.

I am trying to generate a sitemap(s) that can be subbited to search engines for crawling. So when you tell google or bing to go to example.com.sitemap.xml and the engine hits that URL, if the sitemap function in the package is enabled, then a sitemap file is generated and returned with all the current URLs of that site. In a muti-site setup it should do this for only the nodes belonging to the given domain.

Hi @Eaglef90

If you follow the instructions in that link to create a sitemap XML file, that is exactly what search engines consume. If you’re worried about performance of generating this every time, then you can get it to save it to a file on first load, and re-read from the same file subsequently. If a content page gets published or unpublished, just have a notification handler that deletes the file so it gets regenerated next time.

You will need to implement a locking mechanism so only 1 request thread reads and writes to the file at a time.

The alternative is to use output caching in .NET which will cache the response, but again you will need to clear the cache when content is published or unpublished.

Justin

PS.. plenty of sitemap packages on Search on the Umbraco Marketplace if you didn’t want/need to reinvent the wheel.. or for that matter to seek guidance/inspiration in the source code
SeoToolkit is award winning…

For your ask re filebased storage, umbraco needs write access to the Umbraco\Data folder you can always rely on that? If you wanted to generate a static xml file and then update that, but then you’d have fileIO considerations, so IMemoryCache is probably better with a controller route.
And if you did have write access to wwwroot, asp.net helps by serving automatically as static content so bypassing any asp.net overheads..

Constants.SystemDirectories.Data is a constant you can use for that path..
Umbraco-CMS/src/Umbraco.Core/Constants-SystemDirectories.cs at main · umbraco/Umbraco-CMS

For IPublishedCache, you may also want to investigate cache seeding.. Cache Seeding | CMS | Umbraco Documentation

Depending on the use case.. there’s also potentially a downside to caching, say a store with high stock rotation?

You could go with a chunked site map, so rebuilding is mitigated to some extent?

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://www.yoursite.com/sitemap-content.xml</loc>
    <lastmod>2026-06-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://www.yoursite.com/sitemap-products-1.xml</loc>
    <lastmod>2026-06-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://www.yoursite.com/sitemap-products-2.xml</loc>
    <lastmod>2026-06-06</lastmod>
  </sitemap>
</sitemapindex>

ai slop below… :wink:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Web.Common.Controllers;

[Route("sitemap.xml")]
public class SitemapController : UmbracoController
{
    private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
    private readonly IMemoryCache _memoryCache;
    private const int MaxUrlsPerSitemap = 10000; // Adjust chunk size as needed

    public SitemapController(
        IPublishedSnapshotAccessor publishedSnapshotAccessor, 
        IMemoryCache memoryCache)
    {
        _publishedSnapshotAccessor = publishedSnapshotAccessor;
        _memoryCache = memoryCache;
    }

    // 1. Serves the Sitemap Index file pointing to chunks
    [HttpGet]
    public IActionResult SitemapIndex()
    {
        string cacheKey = "Sitemap_Index";
        
        var xml = _memoryCache.GetOrCreate(cacheKey, entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
            
            var builder = new System.Text.StringBuilder();
            builder.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            builder.AppendLine("<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">");

            // Base domain lookup for absolute sitemap links
            var baseUrl = Request.Scheme + "://" + Request.Host;

            // Add static/content sitemap chunk
            builder.AppendLine("  <sitemap>");
            builder.AppendLine($"    <loc>{baseUrl}/sitemap-content.xml</loc>");
            builder.AppendLine("  </sitemap>");

            // Add product sitemap chunks (supports multiple chunks if product count is high)
            int productChunkCount = GetProductChunkCount();
            for (int i = 1; i <= productChunkCount; i++)
            {
                builder.AppendLine("  <sitemap>");
                builder.AppendLine($"    <loc>{baseUrl}/sitemap-products-{i}.xml</loc>");
                builder.AppendLine("  </sitemap>");
            }

            builder.AppendLine("</sitemapindex>");
            return builder.ToString();
        });

        return Content(xml, "application/xml", System.Text.Encoding.UTF8);
    }

    // 2. Serves standard/editorial content sitemap chunk
    [HttpGet("/sitemap-content.xml")]
    public IActionResult ContentSitemap()
    {
        string cacheKey = "Sitemap_Chunk_Content";

        var xml = _memoryCache.GetOrCreate(cacheKey, entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);

            var builder = new System.Text.StringBuilder();
            builder.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            builder.AppendLine("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">");

            if (_publishedSnapshotAccessor.TryGetPublicPublishedSnapshot(out var snapshot) && snapshot.Content != null)
            {
                // Grab all non-product standard pages
                var standardPages = snapshot.Content.GetAtRoot()
                    .SelectMany(x => x.DescendantsOrSelf())
                    .Where(x => x.IsVisible() && x.ContentType.Alias != "product");

                foreach (var page in standardPages)
                {
                    builder.AppendLine("  <url>");
                    builder.AppendLine($"    <loc>{page.Url(mode: UrlMode.Absolute)}</loc>");
                    builder.AppendLine($"    <lastmod>{page.UpdateDate:yyyy-MM-dd}</lastmod>");
                    builder.AppendLine("  </url>");
                }
            }

            builder.AppendLine("</urlset>");
            return builder.ToString();
        });

        return Content(xml, "application/xml", System.Text.Encoding.UTF8);
    }

    // 3. Serves paged product sitemap chunks
    [HttpGet("/sitemap-products-{chunk}.xml")]
    public IActionResult ProductSitemap(int chunk)
    {
        string cacheKey = $"Sitemap_Chunk_Products_{chunk}";

        var xml = _memoryCache.GetOrCreate(cacheKey, entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);

            var builder = new System.Text.StringBuilder();
            builder.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            builder.AppendLine("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">");

            if (_publishedSnapshotAccessor.TryGetPublicPublishedSnapshot(out var snapshot) && snapshot.Content != null)
            {
                // Fetch products and apply chunking (skip/take) to prevent massive memory payloads
                var products = snapshot.Content.GetAtRoot()
                    .SelectMany(x => x.DescendantsOrSelf())
                    .Where(x => x.IsVisible() && x.ContentType.Alias == "product")
                    .Skip((chunk - 1) * MaxUrlsPerSitemap)
                    .Take(MaxUrlsPerSitemap);

                foreach (var product in products)
                {
                    builder.AppendLine("  <url>");
                    builder.AppendLine($"    <loc>{product.Url(mode: UrlMode.Absolute)}</loc>");
                    builder.AppendLine($"    <lastmod>{product.UpdateDate:yyyy-MM-dd}</lastmod>");
                    builder.AppendLine("  </url>");
                }
            }

            builder.AppendLine("</urlset>");
            return builder.ToString();
        });

        return Content(xml, "application/xml", System.Text.Encoding.UTF8);
    }

    private int GetProductChunkCount()
    {
        if (_publishedSnapshotAccessor.TryGetPublicPublishedSnapshot(out var snapshot) && snapshot.Content != null)
        {
            int totalProducts = snapshot.Content.GetAtRoot()
                .SelectMany(x => x.DescendantsOrSelf())
                .Count(x => x.IsVisible() && x.ContentType.Alias == "product");

            if (totalProducts == 0) return 1;
            return (int)Math.Ceiling((double)totalProducts / MaxUrlsPerSitemap);
        }
        return 1;
    }
}

chunk invalidation..

using Microsoft.Extensions.Caching.Memory;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;

public class SitemapCacheInvalidator : 
    INotificationHandler<ContentPublishedNotification>,
    INotificationHandler<ContentUnpublishedNotification>
{
    private readonly IMemoryCache _memoryCache;

    public SitemapCacheInvalidator(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    public void Handle(ContentPublishedNotification notification)
    {
        ProcessEntities(notification.PublishedEntities);
    }

    public void Handle(ContentUnpublishedNotification notification)
    {
        ProcessEntities(notification.UnpublishedEntities);
    }

    private void ProcessEntities(IEnumerable<IContent> entities)
    {
        bool invalidateContent = false;
        bool invalidateProducts = false;

        foreach (var entity in entities)
        {
            if (entity.ContentType.Alias == "product")
            {
                invalidateProducts = true;
            }
            else
            {
                invalidateContent = true;
            }
        }

        // Always invalidate the root index since counts or chunk structures might shift
        _memoryCache.Remove("Sitemap_Index");

        if (invalidateContent)
        {
            // Clear the standard editorial pages chunk
            _memoryCache.Remove("Sitemap_Chunk_Content");
        }

        if (invalidateProducts)
        {
            // Because products are chunked (e.g., Sitemap_Chunk_Products_1, _2, etc.),
            // a new product or a moved product can alter items across multiple pages.
            // Rather than trying to guess the exact chunk, clear all potential product chunks.
            // (Alternatively, use a version token approach or clear a known maximum range).
            ClearAllProductChunks();
        }
    }

    private void ClearAllProductChunks()
    {
        // Assuming a safe max limit of chunks (e.g., up to 20 chunks = 200,000 products)
        for (int i = 1; i <= 20; i++)
        {
            _memoryCache.Remove($"Sitemap_Chunk_Products_{i}");
        }
    }
}

registration

using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Notifications;

public class SitemapComposer : IComposer
{
    public void Compose(IUmbracoManager composition) // Or IRegister builder depending on Umbraco version
    {
        // For modern Umbraco (v10+ / v17)
    }
}

// Cleaner extension syntax style:
public class SitemapComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.AddNotificationHandler<ContentPublishedNotification, SitemapCacheInvalidator>();
        builder.AddNotificationHandler<ContentUnpublishedNotification, SitemapCacheInvalidator>();
    }
}

Thanks for the suggestion on looking at existing packages for reference as well as the note about Umbraco\Data being written accessible so that might be a solution though, as you point out, a site with say a store on it that might end up being a lot of file I/O as well as caching issues. All very good information in this thread to go though and help get things figured out.