15

Can anyone tell me how to create a Sitemap in .NET Core 2?

These articles/alternate link are not working in .NET Core 2.

Sha
  • 2,185
  • 1
  • 36
  • 61
Beginner
  • 189
  • 1
  • 1
  • 7

11 Answers11

27

I found a solution to your question from a sample web application that I was working through. Credit goes to Mads Kristensen. This is a very simplified version of what you are looking for. Put this code in a controller class like HomeController the same way as what you would add an action method.

Here is the method that returns XML:

[Route("/sitemap.xml")]
public void SitemapXml()
{
     string host = Request.Scheme + "://" + Request.Host;

     Response.ContentType = "application/xml";

     using (var xml = XmlWriter.Create(Response.Body, new XmlWriterSettings { Indent = true }))
     {
          xml.WriteStartDocument();
          xml.WriteStartElement("urlset", "http://www.sitemaps.org/schemas/sitemap/0.9");

          xml.WriteStartElement("url");
          xml.WriteElementString("loc", host);
          xml.WriteEndElement();

          xml.WriteEndElement();
     }
}

This will produce the following when you type in http://www.example.com/sitemap.xml:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
     <url>
          <loc>http://www.example.com/</loc>
     </url>
</urlset>

I hope this helps? If you also found something post your solution as an update to your question.

Brendan Vogt
  • 25,678
  • 37
  • 146
  • 234
  • 1
    nice solution without any packages. Works also for ASP.NET Core MVC with .NET Framework – everydayXpert Feb 11 '19 at 08:13
  • 4
    Please note depending on .net core version the AllowSynchronousIO feature could be disabled by default. You can override per request using `var syncIOFeature = HttpContext.Features.Get(); if (syncIOFeature != null) { syncIOFeature.AllowSynchronousIO = true; }` read more here https://learn.microsoft.com/en-us/dotnet/core/compatibility/2.2-3.0 –  Apr 28 '20 at 15:52
  • It would be nice if the function method could be declared async. When you make it async void the Response object ends up being disposed before the function completes. Being able to combine this with an async Task that streamed out the file as its being created would be great. – Steve Wranovsky Aug 02 '22 at 21:17
14

Actually, I prefer to write it in a template file using Razor. Assuming you only have one page, A sample code in .NET Core 3.1 will look like this (.NET core 2 code won't be much different):

<!-- XmlSitemap.cshtml -->
@page "/sitemap.xml"
@using Microsoft.AspNetCore.Http
@{
    var pages = new List<dynamic>
    {
        new { Url = "http://example.com/", LastUpdated = DateTime.Now }
    };
    Layout = null;
    Response.ContentType = "text/xml";
    await Response.WriteAsync("<?xml version='1.0' encoding='UTF-8' ?>");
}

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    @foreach (var page in pages)
    {
        <url>
            <loc>@page.Url</loc>
            <lastmod>@page.LastUpdated.ToString("yyyy-MM-dd")</lastmod>
        </url>
    }
</urlset>

Hope this helps!

Babak
  • 1,274
  • 15
  • 18
  • This is a great solution if you're using MVC. You can create functionality to pass a list of pages as a viewmodel to this Razor view and then generate it on demand. You can also cache it on the back end for improved performance. – Eric Conklin Mar 16 '22 at 00:23
  • You cannot use @page in razor in .Net 5 so I would modify it to a different variable name, otherwise looks good. – Eric Conklin Mar 16 '22 at 01:02
7

Fortunately, there is already a list of pre-built libraries out there. Install this tool https://github.com/uhaciogullari/SimpleMvcSitemap

Then create a new controller like so (there are more examples on the github):

public class SitemapController : Controller
{
    public ActionResult Index()
    {
        List<SitemapNode> nodes = new List<SitemapNode>
        {
            new SitemapNode(Url.Action("Index","Home")),
            new SitemapNode(Url.Action("About","Home")),
            //other nodes
        };

        return new SitemapProvider().CreateSitemap(new SitemapModel(nodes));
    }
}
Sha
  • 2,185
  • 1
  • 36
  • 61
5

The middleware works fine, but needed a minor fix.

if (context.Request.Path.Value.Equals("/sitemap.xml", StringComparison.OrdinalIgnoreCase))
{
    // Implementation
}
else
    await _next(context);

I created a new project then after adding the middleware and running, I entered http://localhost:64522/sitemap.xml into the browser I got the following result:

<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>http://localhost:64522/home/index</loc>
    <lastmod>2018-05-13</lastmod>
  </url>
  <url>
    <loc>http://localhost:64522/home/about</loc>
    <lastmod>2018-05-13</lastmod>
  </url>
  <url>
    <loc>http://localhost:64522/home/contact</loc>
    <lastmod>2018-05-13</lastmod>
  </url>
  <url>
    <loc>http://localhost:64522/home/privacy</loc>
    <lastmod>2018-05-13</lastmod>
  </url>
  <url>
    <loc>http://localhost:64522/home/error</loc>
    <lastmod>2018-05-13</lastmod>
  </url>
</urlset>
Mark G
  • 2,848
  • 1
  • 24
  • 32
2

Dynamic site map "sitemap-blog.xml" for Blog section and 24h cache. (ASP.NET Core 3.1)

  • sitemap.xml exists in wwwroot (generated by xml-sitemaps.com or ...).
  • sitemap-blog.xml generate dynamically.

robots.txt

User-agent: *
Disallow: /Admin/
Disallow: /Identity/
Sitemap: https://example.com/sitemap.xml
Sitemap: https://example.com/sitemap-blog.xml

Startup.cs

services.AddMemoryCache();

HomeController.cs

namespace MT.Controllers
{
    public class HomeController : Controller
    {
        private readonly ApplicationDbContext _context;
        private readonly IMemoryCache _cache;

        public HomeController(
            ApplicationDbContext context,
            IMemoryCache cache)
        {
            _context = context;
            _cache = cache;

        }

        [Route("/sitemap-blog.xml")]
        public async Task<IActionResult> SitemapBlog()
        {
            string baseUrl = $"{Request.Scheme}://{Request.Host}{Request.PathBase}";
            string segment = "blog";
            string contentType = "application/xml";

            string cacheKey = "sitemap-blog.xml";

            // For showing in browser (Without download)
            var cd = new System.Net.Mime.ContentDisposition
            {
                FileName = cacheKey,
                Inline = true,
            };
            Response.Headers.Append("Content-Disposition", cd.ToString());

            // Cache
            var bytes = _cache.Get<byte[]>(cacheKey);
            if (bytes != null)
                return File(bytes, contentType);

            var blogs = await _context.Blogs.ToListAsync();

            var sb = new StringBuilder();
            sb.AppendLine($"<?xml version=\"1.0\" encoding=\"utf-8\"?>");
            sb.AppendLine($"<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"");
            sb.AppendLine($"   xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
            sb.AppendLine($"   xsi:schemaLocation=\"http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd\">");

            foreach (var m in blogs)
            {
                var dt = m.LastModified;
                string lastmod = $"{dt.Year}-{dt.Month.ToString("00")}-{dt.Day.ToString("00")}";

                sb.AppendLine($"    <url>");

                sb.AppendLine($"        <loc>{baseUrl}/{segment}/{m.Slug}</loc>");
                sb.AppendLine($"        <lastmod>{lastmod}</lastmod>");
                sb.AppendLine($"        <changefreq>daily</changefreq>");
                sb.AppendLine($"        <priority>0.8</priority>");

                sb.AppendLine($"    </url>");
            }

            sb.AppendLine($"</urlset>");

            bytes = Encoding.UTF8.GetBytes(sb.ToString());

            _cache.Set(cacheKey, bytes, TimeSpan.FromHours(24));
            return File(bytes, contentType);
        }
    }
}
Haddad
  • 301
  • 3
  • 5
  • With a large sitemap, the one drawback I'm seeing here is that the whole sitemap must be in memory before the response is returned, it would be nice if this solution could "stream" the xml file out if its large as its being created. – Steve Wranovsky Aug 02 '22 at 21:14
2

The below code is working for ASP.NET Core 2.2.

 public class SitemapUrl
    {
        public string Page { get; set; }

        public DateTime? LastModifyed { get; set; }

        /*
            always
            hourly
            daily
            weekly
            monthly
            yearly
            never
        */
        public string ChangeFreq { get; set; }

        public float Priority { get; set; } = 0.5f;
    }

    public class SitemapResult : ActionResult
    {
        private readonly IEnumerable<SitemapUrl> _urls;

        public SitemapResult(IEnumerable<SitemapUrl> urls)
        {
            _urls = urls;
        }

        public override async Task ExecuteResultAsync(ActionContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
            var response = context.HttpContext.Response;
            response.ContentType = "application/xml; charset=utf-8";

            var settings = new XmlWriterSettings() { Async = true, Encoding = Encoding.UTF8, Indent = false };
            using (var writer = XmlWriter.Create(response.Body, settings))
            {
                WriteToXML(writer);
                await writer.FlushAsync();
            }
        }

        private void WriteToXML(XmlWriter writer)
        {
            writer.WriteStartDocument();
            // Write the urlset.
            writer.WriteStartElement("urlset", "http://www.sitemaps.org/schemas/sitemap/0.9");
            // url element
            foreach (var item in _urls)
            {
                writer.WriteStartElement("url");
                // loc
                writer.WriteStartElement("loc");
                writer.WriteValue(item.Page);
                writer.WriteEndElement();
                // changefreq
                if (!string.IsNullOrEmpty(item.ChangeFreq))
                {
                    writer.WriteStartElement("changefreq");
                    writer.WriteValue(item.ChangeFreq);
                    writer.WriteEndElement();
                }
                // lastmod
                if (item.LastModifyed.HasValue)
                {
                    writer.WriteStartElement("lastmod");
                    writer.WriteValue(item.LastModifyed.Value.ToString("yyyy-MM-dd"));
                    writer.WriteEndElement();
                }

                // priority
                writer.WriteStartElement("priority");
                writer.WriteValue(item.Priority);
                writer.WriteEndElement();

                writer.WriteEndElement();
            }
            writer.WriteEndElement();
            writer.WriteEndDocument();
        }
    }

after then call the SitemapResult in MVC's Controller class.

public IActionResult Sitemap(){
    return new SitemapResult(new SitemapUrl[] { new SitemapUrl() { } });
}

In ASP.NET 3.0+ just remove async operation for XmlWriterSettings or AllowSynchronousIO disabled in all servers

I uses SitemapHub sitemap tool to build XML sitemap for my multiple websites, without coding, saving my time.

zhengchun
  • 1,261
  • 13
  • 19
2

SEOHelper is a nuget library for SEO management.

Install-Package AspNetCore.SEOHelper

SEOHelper package provides SitemapNode class for set a URL and CreateSitemapXML method for create sitemap.xml.

var list = new List<SitemapNode>();  
list.Add(new SitemapNode { LastModified = DateTime.UtcNow, Priority = 0.8, Url = "https://www.example.com/page 1", Frequency = SitemapFrequency.Daily });  
list.Add(new SitemapNode { LastModified = DateTime.UtcNow, Priority = 0.9, Url = "https://www.example.com/page2", Frequency = SitemapFrequency.Yearly });  
new SitemapDocument().CreateSitemapXML(list, _env.ContentRootPath);  
hamid_reza hobab
  • 925
  • 9
  • 21
1

Let's take two approachs for achieving the desired result.

First, those articles use a middleware approach. There´s and easy solution using a controller and SimpleMvcSitemap nuget package

public class SitemapController : Controller
{
    public ActionResult Index()
    {
        List<SitemapNode> nodes = new List<SitemapNode>
        {
            new SitemapNode(Url.Action("Index","Home")),
            new SitemapNode(Url.Action("About","Home")),
            //other nodes
        };

        return new SitemapProvider().CreateSitemap(new SitemapModel(nodes));
    }
}

Second part is to get all controllers and actions dynamically using reflection. Using the iaspnetcore example, here is how to get the list of controllers and actions

            // get available controllers
            var controllers = Assembly.GetExecutingAssembly().GetTypes()
                .Where(type => typeof(Controller).IsAssignableFrom(type)
                || type.Name.EndsWith("controller")).ToList();

            foreach (var controller in controllers)
            {
                var controllerName = controller.Name.Replace("Controller", "");

                // get available methods  in controller
                var methods = controller.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
                    .Where(method => typeof(IActionResult).IsAssignableFrom(method.ReturnType));
                foreach (var method in methods)
                {
                    var myRoute = Url.Action(method.Name, controllerName);
                }
            }

And putting it all together we have this code

/// <summary>
/// Base URL Provider for sitemap. Replace with your domain
/// </summary>
public class BaseUrlProvider : IBaseUrlProvider
{
    public Uri BaseUrl => new Uri("https://example.com");
}

public class SitemapController : Controller
{

    [Route("sitemap.xml")]
    public ActionResult Index()
    {
        List<SitemapNode> nodes = new List<SitemapNode>();


        // get available contrtollers
        var controllers = Assembly.GetExecutingAssembly().GetTypes()
                .Where(type => typeof(Controller).IsAssignableFrom(type)
                || type.Name.EndsWith("controller")).ToList();

        foreach (var controller in controllers)
        {
            // get available methods
            var methods = controller.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
                .Where(method => typeof(IActionResult).IsAssignableFrom(method.ReturnType));

            foreach (var method in methods)
            {
                // add route name in sitemap
                nodes.Add(new SitemapNode(Url.Action(method.Name, controllerName)));
            }
        }

        return new SitemapProvider(new BaseUrlProvider()).CreateSitemap(new SitemapModel(nodes));
    }
}

The using list:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using SimpleMvcSitemap;
using SimpleMvcSitemap.Routing;

Finally just open route e.g.:

https://localhost:44312/sitemap.xml
Bruno Matuk
  • 562
  • 5
  • 11
1

startup

 services.AddMvcCore(options =>
        {
            options.OutputFormatters.Clear(); // Remove json for simplicity
            options.OutputFormatters.Add(new MyCustomXmlSerializerOutputFormatter());
        });
app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
       name: "sitemapxml",
       pattern: "/sitemap.xml",
       defaults: new { controller = "Home", action = "SitemapXML" }
       );

customformatter

  public class MyCustomXmlSerializerOutputFormatter : XmlSerializerOutputFormatter
{
    protected override void Serialize(XmlSerializer xmlSerializer, XmlWriter xmlWriter, object value)
    {

        xmlSerializer = new XmlSerializer(typeof(List<url>), new XmlRootAttribute("urlset"));

        xmlSerializer.Serialize(xmlWriter, value);
    }
}

urlclass

public class url
{
    

    public string changefreq { get; set; }
    public DateTime? lastModified { get; set; }
    public double? priority { get; set; }
    public string loc { get; set; }
}

controller

    [HttpGet]
    [Produces("application/xml")]
    public ActionResult<List<url>> SitemapXML() 
    {
        var list = new List<url>();

        var dokumanlar = _dokumanCategoryService.GetAll().Where(i => i.Yatirimci == 1).ToList();


        

        foreach (var dokuman in dokumanlar)
        {
            var dokumanurl = dokuman.SeoUrl;

            var culture = dokuman.Culture;

            if (dokuman.Culture == "tr")
            {
                list.Add(new url { lastModified = DateTime.UtcNow, priority = 0.8, loc = $"https://example.com/yatirimci-iliskileri/{dokumanurl}", changefreq = "always" });
            }
            else
            {
                list.Add(new url { lastModified = DateTime.UtcNow, priority = 0.8, loc = $"https://example.com/{culture}/investor-relations/{dokumanurl}", changefreq = "always" });
            }


        }


        

        return list;
    }
Saltukz
  • 43
  • 4
0

If you are using .net core 2 and above do this:

Add this to .csproj file

then in your Program.cs file add the reference using X.Web.Sitemap;

In your Program.cs file do this inside the Main method:

var sitemap = new Sitemap();

               sitemap.Add(new Url
               {
                    ChangeFrequency = ChangeFrequency.Daily,
                    Location = "https://www.website.com",
                    Priority = 0.5,
                    TimeStamp = DateTime.Now
               });

               sitemap.Add(CreateUrl("https://www.website.com/about"));
               sitemap.Add(CreateUrl("https://www.website.com/services"));
               sitemap.Add(CreateUrl("https://www.website.com/products"));
               sitemap.Add(CreateUrl("https://www.website.com/casestudies"));
               sitemap.Add(CreateUrl("https://www.website.com/blogs"));
               sitemap.Add(CreateUrl("https://www.website.com/contact"));

               //Save sitemap structure to file
               sitemap.Save(@"wwwroot\sitemap.xml");

               //Split a large list into pieces and store in a directory
               //sitemap.SaveToDirectory(@"d:\www\summituniversity.edu.ng\sitemaps");

               //Get xml-content of file
               Console.Write(sitemap.ToXml());
               Console.ReadKey();

Out side the main method do this:

private static Url CreateUrl(string url)
          {
               return new Url
               {
                    ChangeFrequency = ChangeFrequency.Daily,
                    Location = url,
                    Priority = 0.5,
                    TimeStamp = DateTime.Now
               };
          }
Ismail Umar
  • 836
  • 1
  • 11
  • 10
0

The most elegant way by far I've found with .Net Core 3.x is to use ParkSquare.AspNetCore.Sitemap. This creates a sitemap.xml and robots.txt dynamically based on your defined routes.

In startup.cs, register the middleware:

app.UseSitemap();

To exclude anything, you can decorate the controller class to exclude everything in that controller, or specific routes:

// All routes in this controller will be ignored
[SitemapExclude]
public class BlahController : Controller
{
    [Route("some-route")]
    public IActionResult Something()
    {
        return View();
    }
}

public class BlahController : Controller
{
    [SitemapExclude]
    [Route("some-route")]
    public IActionResult Ignored()
    {
        return View();
    }

    [Route("some-other-route")]
    public IActionResult NotIgnored()
    {  
        return View();
    }
}
m33sy
  • 9
  • 3