Whenever you create any web-based application you need to submit the Sitemap of that website links/Url to Google Webmasters so that Google bot can crawl it and other internet users can find using Google search engine. So, here in this post, I am going to explain to you how you can create XML dynamically in ASP.NET MVC C# web application.
Step 1: Create a Project in Visual Studio
Open our Visual Studio IDE, create a new project, by navigating to "File"-> "New"-> "Project".
Select "Web" in the left pane and "ASP.NET Web application" in the right pane, provide a name for your web application(In my example it's "XMLSitemapProject"), Click "OK".
Step 2: Create the Helper Folder and Helper classes
To make sure everything work's fine we would need some helper methods, so first, create a new folder in your project with Name "Helper".
First Helper would be to check Regex of the URL while generating Slug, so let's create a Class with name RegexUtils.cs and add the code below in it
public static class RegexUtils
{
/// <summary>
/// A regular expression for validating slugs.
/// Does not allow leading or trailing hypens or whitespace
/// </summary>
public static readonly Regex SlugRegex = new Regex(@"(^[a-z0-9])([a-z0-9_-]+)*([a-z0-9])$");
}
Second helper method would be to Extend StringExtensions in our app, which helps us to validate String value is null or not and generate Slug URL for us with the help of above create Regex, so let's add a new .cs file with name StringExtensions
public static class StringExtensions
{
/// <summary>
/// A nicer way of calling <see cref="System.String.IsNullOrEmpty(string)"/>
/// </summary>
/// <param name="value">The string to test.</param>
/// <returns>true if the value parameter is null or an empty string (""); otherwise, false.</returns>
public static bool IsNullOrEmpty(this string value)
{
return string.IsNullOrEmpty(value);
}
/// <summary>
/// A nicer way of calling the inverse of <see cref="System.String.IsNullOrEmpty(string)"/>
/// </summary>
/// <param name="value">The string to test.</param>
/// <returns>true if the value parameter is not null or an empty string (""); otherwise, false.</returns>
public static bool IsNotNullOrEmpty(this string value)
{
return !value.IsNullOrEmpty();
}
/// <summary>
/// Slugifies a string
/// </summary>
/// <param name="value">The string value to slugify</param>
/// <param name="maxLength">An optional maximum length of the generated slug</param>
/// <returns>A URL safe slug representation of the input <paramref name="value"/>.</returns>
public static string ToSlug(this string value, int? maxLength = null)
{
// Ensure.Argument.NotNull(value, "value");
// if it's already a valid slug, return it
if (RegexUtils.SlugRegex.IsMatch(value))
return value;
return GenerateSlug(value, maxLength);
}
/// <summary>
/// Credit for this method goes to http://stackoverflow.com/questions/2920744/url-slugify-alrogithm-in-cs
/// </summary>
private static string GenerateSlug(string value, int? maxLength = null)
{
// prepare string, remove accents, lower case and convert hyphens to whitespace
var result = RemoveAccent(value).Replace("-", " ").ToLowerInvariant();
result = Regex.Replace(result, @"[^a-z0-9\s-]", string.Empty); // remove invalid characters
result = Regex.Replace(result, @"\s+", " ").Trim(); // convert multiple spaces into one space
if (maxLength.HasValue) // cut and trim
result = result.Substring(0, result.Length <= maxLength ? result.Length : maxLength.Value).Trim();
return Regex.Replace(result, @"\s", "-"); // replace all spaces with hyphens
}
private static string RemoveAccent(string value)
{
var bytes = Encoding.GetEncoding("Cyrillic").GetBytes(value);
return Encoding.ASCII.GetString(bytes);
}
}
Above code is mostly explained with the help of Comments.
Now, Add one more class named as PathUtils, which will help us combine paths, so here is the code for it
/// <summary>
/// Utility methods for working with resource paths
/// </summary>
public static class PathUtils
{
/// <summary>
/// Makes a filename safe for use within a URL
/// </summary>
public static string MakeFileNameSafeForUrls(string fileName)
{
var extension = Path.GetExtension(fileName);
var safeFileName = Path.GetFileNameWithoutExtension(fileName).ToSlug();
return Path.Combine(Path.GetDirectoryName(fileName), safeFileName + extension);
}
/// <summary>
/// Combines two URL paths
/// </summary>
public static string CombinePaths(string path1, string path2)
{
if (path2.IsNullOrEmpty())
{
return path1;
}
if (path1.IsNullOrEmpty())
return path2;
if (path2.StartsWith("http://") || path2.StartsWith("https://"))
return path2;
var ch = path1[path1.Length - 1];
if (ch != '/')
return (path1.TrimEnd('/') + '/' + path2.TrimStart('/'));
return (path1 + path2);
}
}
That's it, we are done with the helper class.
Step 3: Generate Model and interface for Sitemap
We definitely need some models to generate dynamic XML using C#, so create an Interface first, let's name it ISitemapItem, in which we will add all the properties of the XML node, required to create Sitemap node.
A Sitemap node looks like this
<url>
<loc>http://www.domain.com /</loc>
<lastmod>2017-01-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
So the interface code would be as below
/// <summary>
/// An interface for sitemap items
/// </summary>
public interface ISitemapItem
{
/// <summary>
/// URL of the page.
/// </summary>
string Url { get; }
/// <summary>
/// The date of last modification of the file.
/// </summary>
DateTime? LastModified { get; }
/// <summary>
/// How frequently the page is likely to change.
/// </summary>
SitemapChangeFrequency? ChangeFrequency { get; }
/// <summary>
/// The priority of this URL relative to other URLs on your site. Valid values range from 0.0 to 1.0.
/// </summary>
double? Priority { get; }
}
Create another Interface with name ISitemapGenerator.cs
public interface ISitemapGenerator
{
XDocument GenerateSiteMap(IEnumerable<ISitemapItem> items);
}
Now we would need an Enum Class which will help us define Change frequency of Sitemap, so let's create Enum for it.
/// <summary>
/// How frequently the page is likely to change.
/// This value provides general information to search engines and may not correlate exactly to how often they crawl the page.
/// </summary>
/// <remarks>
/// The value "always" should be used to describe documents that change each time they are accessed. The value "never" should be used to describe archived URLs.
/// </remarks>
public enum SitemapChangeFrequency
{
Always,
Hourly,
Daily,
Weekly,
Monthly,
Yearly,
Never
}
We have all the Interface defined now, let's add the Class which defines the XML node details (SitemapItem .cs
) which Inherit Interface ISitemapItem
/// <summary>
/// Represents a sitemap item.
/// </summary>
public class SitemapItem : ISitemapItem
{
/// <summary>
/// Creates a new instance of <see cref="SitemapItem"/>
/// </summary>
/// <param name="url">URL of the page. Optional.</param>
/// <param name="lastModified">The date of last modification of the file. Optional.</param>
/// <param name="changeFrequency">How frequently the page is likely to change. Optional.</param>
/// <param name="priority">The priority of this URL relative to other URLs on your site. Valid values range from 0.0 to 1.0. Optional.</param>
/// <exception cref="System.ArgumentNullException">If the <paramref name="url"/> is null or empty.</exception>
public SitemapItem(string url, DateTime? lastModified = null, SitemapChangeFrequency? changeFrequency = null, double? priority = null)
{
// Ensure.Argument.NotNullOrEmpty(url, "url");
Url = url;
LastModified = lastModified;
ChangeFrequency = changeFrequency;
Priority = priority;
}
/// <summary>
/// URL of the page.
/// </summary>
public string Url { get; protected set; }
/// <summary>
/// The date of last modification of the file.
/// </summary>
public DateTime? LastModified { get; protected set; }
/// <summary>
/// How frequently the page is likely to change.
/// </summary>
public SitemapChangeFrequency? ChangeFrequency { get; protected set; }
/// <summary>
/// The priority of this URL relative to other URLs on your site. Valid values range from 0.0 to 1.0.
/// </summary>
public double? Priority { get; protected set; }
}
We have all the classes and helper method, let's use it to generate the XML sitemap and its node by creating a class (SitemapGenerator.cs
)
/// <summary>
/// A class for creating XML Sitemaps (see http://www.sitemaps.org/protocol.html)
/// </summary>
public class SitemapGenerator : ISitemapGenerator
{
private static readonly XNamespace xmlns = "http://www.sitemaps.org/schemas/sitemap/0.9";
private static readonly XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";
public XDocument GenerateSiteMap(IEnumerable<ISitemapItem> items)
{
//Ensure.Argument.NotNull(items, "items");
var sitemap = new XDocument(
new XDeclaration("1.0", "utf-8", "yes"),
new XElement(xmlns + "urlset",
new XAttribute("xmlns", xmlns),
new XAttribute(XNamespace.Xmlns + "xsi", xsi),
new XAttribute(xsi + "schemaLocation", "http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"),
from item in items
select CreateItemElement(item)
)
);
return sitemap;
}
public XElement CreateItemElement(ISitemapItem item)
{
var itemElement = new XElement(xmlns + "url", new XElement(xmlns + "loc", item.Url.ToLowerInvariant()));
// all other elements are optional
if (item.LastModified.HasValue)
itemElement.Add(new XElement(xmlns + "lastmod", item.LastModified.Value.ToString("yyyy-MM-dd")));
if (item.ChangeFrequency.HasValue)
itemElement.Add(new XElement(xmlns + "changefreq", item.ChangeFrequency.Value.ToString().ToLower()));
if (item.Priority.HasValue)
itemElement.Add(new XElement(xmlns + "priority", item.Priority.Value.ToString("F1", CultureInfo.InvariantCulture)));
return itemElement;
}
}
That's it, we are done with our main XML generator method.
Finally, we would need a Class which extends ActionResult and give's us XML directly by calling Action of Controller, so create a SitemapResult.cs
as below
public class SitemapResult : ActionResult
{
private readonly IEnumerable<ISitemapItem> items;
private readonly ISitemapGenerator generator;
public SitemapResult(IEnumerable<ISitemapItem> items) : this(items, new SitemapGenerator())
{
}
public SitemapResult(IEnumerable<ISitemapItem> items, ISitemapGenerator generator)
{
this.items = items;
this.generator = generator;
}
public override void ExecuteResult(ControllerContext context)
{
var response = context.HttpContext.Response;
response.ContentType = "text/xml";
response.ContentEncoding = Encoding.UTF8;
using (var writer = new XmlTextWriter(response.Output))
{
writer.Formatting = Formatting.Indented;
var sitemap = generator.GenerateSiteMap(items);
sitemap.WriteTo(writer);
}
}
}
Step 4: Create a Controller which returns XML when calling its ActionMethod call
Let's create a Controller call it SitemapController, right click on your projects Controller Folder, Select "Add", then select "Controller", name it(SitemapController) and click "Ok"
and use the below code
// GET: Sitemap
public ActionResult Index()
{
var sitemapItems = new List<SitemapItem> {
new SitemapItem(Url.Action("index", "home"), changeFrequency: SitemapChangeFrequency.Always, priority: 1.0),
new SitemapItem(Url.Action("about", "home"), lastModified: DateTime.Now),
new SitemapItem(PathUtils.CombinePaths(Request.Url.GetLeftPart(UriPartial.Authority), "/home/list"))
};
return new SitemapResult(sitemapItems);
}
the above method directly returns us the sitemap, so build your project, run it in the browser and Navigate to URL "http://localhost:57630/sitemap", you will see the output as below
That's it, we have the sitemap, I have referenced file from here but you can still proceed further if you want to create a separate XML file
Step 5: Create a Sitemap XML file and Add nodes in it dynamically
If your site is static or doesn't change a lot & you needed to generate sitemap XML for only one time using C#, then above method is fine, but what if you need a separate sitemap.xml file and you want to add more nodes in sitemap regularly as soon as new a web-page is added dynamically in your application, which you want Google to index? So, for that, we need to do some more changes.
Go to your Sitemap controller and add the code below to first generate sitemap's basic nodes and create a separate XML file for it
public ActionResult GenerateSiteMap()
{
var sitemapItems = new List<SitemapItem> {
new SitemapItem(Url.Action("index", "home"), changeFrequency: SitemapChangeFrequency.Always, priority: 1.0),
new SitemapItem(Url.Action("about", "home"), lastModified: DateTime.Now),
new SitemapItem(PathUtils.CombinePaths(Request.Url.GetLeftPart(UriPartial.Authority), "/home/list"))
};
SitemapGenerator sg = new SitemapGenerator();
var doc= sg.GenerateSiteMap(sitemapItems);
doc.Save(Server.MapPath("~/Sitemap.xml"));
return RedirectToAction("Index","Home");
}
Build your project and navigate to URL "http://localhost:57630/sitemap/GenerateSiteMap", it will redirect your to Home page of the application, but in your project's root folder you can see a new file get's added with name "Sitemap.xml", as shown below
now, the last part for adding nodes dynamically, let's create another ActionMethod in your controller to add a sitemap node
public ActionResult AddNewSitemapElement()
{
SitemapGenerator sg = new SitemapGenerator();
//create a sitemap item
var siteMapItem = new SitemapItem(Url.Action("NewAdded", "NewController"), changeFrequency: SitemapChangeFrequency.Always, priority: 1.0);
//Get the XElement from SitemapGenerator.CreateItemElement
var NewItem = sg.CreateItemElement(siteMapItem);
//create XMLdocument element to add the new node in the file
XmlDocument document = new XmlDocument();
//load the already created XML file
document.Load(Server.MapPath("~/Sitemap.xml"));
//convert XElement into XmlElement
XmlElement childElement = document.ReadNode(NewItem.CreateReader()) as XmlElement;
XmlNode parentNode = document.SelectSingleNode("urlset");
//This line of code get's urlset with it's last child and append the new Child just before the last child
document.GetElementsByTagName("urlset")[0].InsertBefore(childElement, document.GetElementsByTagName("urlset")[0].LastChild);
//save the updated file
document.Save(Server.MapPath("~/Sitemap.xml"));
return RedirectToAction("Index", "Home");
}
Build your project and navigate to URL "http://localhost:57630/sitemap/AddNewSitemapElement", it will return to home page but now open the already created "Sitemap.xml" file in your root folder of the project, it should have new node added just before the last node.
so our new node got added as required, that's it, feel free to ask any questions related to this article in question's section or comment below.