Google Sitemap Plug-in for Graffiti
One of missing features for Graffiti Beta 1 is the lack of Google sitemap generation for the site and it's not listed for Beta roadmap as well.
As I liked to play around Graffiti APIs and extend it to some extent, decided to create a plug-in to automatically generate a Google sitemap for the site. Thank to some built-in extensibility options like plug-ins and the simple and powerful APIs of Graffiti and ease of their development, this could be done quickly. Playing around Graffiti development, I faced with some questions that I asked from Scott who helped me solve them easily. Thank you, Scott!
Here I give a short description about the development process of this Google sitemap plug-in as a practice for developing plug-ins for Graffiti.
There are two built-in extensibility options for Graffiti: widgets and plug-ins.
Widgets are obviously some pieces of HTML code that can be added to sidebars and aren't my case here. But plug-ins are very similar to ASP.NET HttpModules or Community Server CSModules. In fact it provides a mechanism to fire your own logic whenever an event occurs in your application.
But the most important point about Graffiti plug-ins which is also a positive point and a difference for them is the fact that they can be deployed easily. For HttpModules or CSModules you need to deploy your assembly to server and configure your application configuration file but in Graffiti you just need to deploy that assembly and no longer need to configure your application. Graffiti takes care about this and looks for a specific type and lists it in Plug-Ins page.
Graffiti plug-ins must be derived from GraffitiEvent base class (located in Graffiti.Core namespace) and override its Init method. Inside Init method you can use its GraffitiApplication parameter to add your event handler for different events based on your needs. Full list of available events for GraffitiApplicaiton and their description is available here.
On the other hand, Graffiti provides another mechanism to let you create customization options for your plug-ins and widgets out of the box. So if you want to ask for user's inputs in your widgets and plug-ins then you don't need to write a lot of code and make your deployment process harder because both Widget and GraffitiEvent classes are derived from EditableForm base class. EditableForm is a class that lets you create a configuration page for your widgets and plug-ins and then add some built-in controls to this page (you can write your own controls as well) to store their inputs into database as XML data and read them later from there. However, this was a short description about EditableForm but I haven't used for my plug-in because didn't need any input from user.

Implementation of my plug-in is simple. After deriving my main SitemapGenerator class from GraffitiEvent, I override some properties and methods from GraffitiEvent class.
using System;
using System.Collections.Generic;
using System.Text;
using Graffiti.Core;
using System.Xml;
using System.IO;
using System.Collections.Specialized;
using System.Web;
namespace Graffiti.GoogleSitemap
{
public class SitemapGenerator : GraffitiEvent
{
By overriding Name, Description and IsEditable properties from GraffitiEvent base class I'm able to change the default texts that appear in control panel for plug-in name and description (defaults are equal to type names) and specify if the plug-in has an edit form (default is false, though).
public override string Name
{
get
{
return "Google Sitemap";
}
}
public override string Description
{
get
{
return "Google Sitemap generator for Graffiti Beta 1 by Keyvan Nayyeri (http://nayyeri.net)";
}
}
public override bool IsEditable
{
get
{
return false;
}
}
The next step would be overriding the Init method and adding my own event handler to AfterCommit event. But why AfterCommit? Because it lets me run my code every time my database is updated (by adding a new item or updating an existing one). So whenever I add data to my database, my sitemap XML file can be generated again. In the GenerateSitemap method I try to get the XML content of my sitemap file and store it into an XML file in the root of the site named sitemap.xml and log any possible exception.
public override void Init(GraffitiApplication ga)
{
ga.AfterCommit += new DataObjectEventHandler(ga_AfterCommit);
}
void ga_AfterCommit(DataBuddyBase dataObject, EventArgs e)
{
GenerateSitemap();
}
private void GenerateSitemap()
{
string xml = GetXml();
HttpContext context = HttpContext.Current;
try
{
Graffiti.Core.Util.CreateFile
(context.Server.MapPath("~/sitemap.xml"), xml);
}
catch (Exception ex)
{
Log.Error("Error in Google Sitemap plug-in", ex.ToString());
}
}
And the main logic to generate the XML content of sitemap file is located in GetXml function which simply returns the string value of the XML content.
private string GetXml()
{
StringBuilder sb = new StringBuilder();
StringWriterWithEncoding stringWriter =
new StringWriterWithEncoding(sb, Encoding.UTF8);
XmlWriter xmlWriter = XmlWriter.Create(stringWriter);
// Start document
xmlWriter.WriteStartDocument();
// <urlset>
xmlWriter.WriteStartElement("urlset",
"http://www.sitemaps.org/schemas/sitemap/0.9");
// Homepage
// <url>
xmlWriter.WriteStartElement("url");
// <loc />
xmlWriter.WriteElementString("loc",
new Macros().FullUrl(new Urls().Home));
// <lastmod />
xmlWriter.WriteElementString("lastmod",
DateTime.Now.ToString("yyyy-MM-dd"));
// <changefreq />
xmlWriter.WriteElementString("changefreq", "daily");
// <priority />
xmlWriter.WriteElementString("priority", "0.5");
// </url>
xmlWriter.WriteEndElement();
// Categories
CategoryCollection categories = CategoryCollection.FetchAll();
foreach (Category category in categories)
{
// <url>
xmlWriter.WriteStartElement("url");
// <loc />
xmlWriter.WriteElementString("loc",
new Macros().FullUrl(category.Url));
// <lastmod />
xmlWriter.WriteElementString("lastmod",
DateTime.Now.ToString("yyyy-MM-dd"));
// <changefreq />
xmlWriter.WriteElementString("changefreq", "daily");
// <priority />
xmlWriter.WriteElementString("priority", "0.5");
// </url>
xmlWriter.WriteEndElement();
}
// Posts
PostCollection posts = PostCollection.FetchAll();
posts.Sort(delegate(Post post1, Post post2)
{
return Comparer<DateTime>.Default.Compare(post2.Published, post1.Published);
});
foreach (Post post in posts)
{
if ((post.IsPublished) && (!post.IsDeleted) && (!post.IsDirty))
{
// <url>
xmlWriter.WriteStartElement("url");
// <loc />
xmlWriter.WriteElementString("loc",
new Macros().FullUrl(post.Url));
// <lastmod />
xmlWriter.WriteElementString("lastmod",
post.ModifiedOn.ToString("yyyy-MM-dd"));
// <changefreq />
xmlWriter.WriteElementString("changefreq", "daily");
// <priority />
xmlWriter.WriteElementString("priority", "0.5");
// </url>
xmlWriter.WriteEndElement();
}
}
// </urlset>
xmlWriter.WriteEndElement();
xmlWriter.Flush();
xmlWriter.Close();
stringWriter.Flush();
return stringWriter.ToString();
}
Note that I used my own custom StringWriter named StringWriterWithEncoding to be able to create my sitemap file with UTF-8 encoding rather than the default UTF-16 encoding which .NET uses to generate XML files. The process of adding elements with an XmlWriter and this custom StringWriter is easy to understand but how about the Graffiti part of the code?
Here in three steps I create appropriate XML elements for homepage URL, category pages and individual posts
At first step I simply add a single element for homepage URL by passing the Urls.Home value to Macros.FullUrl. Macros.FullUrl method returns the full URL of the passed absolute path of a page and I used it in my code to get the real URL of homepage (and other pages).
In the second step I generate XML elements for categories. CategoryCollection is the main class to work with category collections and I used it in order to iterate through its items (which are of Category type) and then added the URL of each category page to the sitemap. CategoryCollection has a FetchAll method which fetches all categories from database.
And in the last step I iterated through a PostCollection to add all published posts to the sitemap. Like CategoryCollection, PostCollection has also a FetchAll method to retrieve all posts but it fetches all posts with same order as their order in the database so I used my own Comparer to sort items based on published date descending. It doesn't matter to sort items based on date descending but I think this way it has a better structure.
This plug-in works easily on the server and generates Google sitemaps like mine which is available here.
You can download Google Sitemap plug-in for Graffiti including its binary and source code files from here. To deploy a Graffiti plug-in, you just need to copy the binary file to bin folder on your server.
[advertisement] Axosoft OnTime 2008 is four developer tools in one: bug tracking, project wiki, feature management, and help desk. It manages your development process so developers can focus on coding. Installed or Hosted – Free Single-user license -- Free 30-day team trial.
9 Comments : 12.12.07
Feedbacks
i have enabled your plugin but in logs i get an error.
can i send log on your email?
Good morning Keyvan.
i have noticed that posts not marked under a category (uncategorized) do not show up. my sitemap (http://blog.lotas-smartman.net/sitemap.xml) is mostly empty. most of my posts where imported and are under the uncategorized category... am i missing something?
--Tiernan
@Tiernan:
No, you're not missing anything. Originally uncategorized posts were a part of sitemap but a few guys requested to not include them in the sitemap.
However, I'll make this optional (via Control Panel) for the next version which would be released as soon as Telligent releases Graffiti 1.1 (I think it won't take longer than 10-12 days).
@Keyvan: Thanks! i might actually play around with the code my self and see what i can get it to do... if i cant get it working, i will wait for the next version! :)
@Tiernan:
It would be easy but if you had any issues, just ping me and I'll send you a quick update that works.
Thanks for sharing this Keyvan! This was a big help.
I made a little tweak to your GetXml method so that each post could have a custom priority setting.
First, I added a Global Custom field named "SiteMap Priority". Then, I changed your code that writes the posts:
<code>
// Get the priority from the custom field named "SiteMap Priority"
decimal siteMapPriorityNumber;
string siteMapPriority;
if (decimal.TryParse(post.Custom("SiteMap Priority"), out siteMapPriorityNumber)
&& siteMapPriorityNumber >= 0
&& siteMapPriorityNumber <= 1)
siteMapPriority = siteMapPriorityNumber.ToString();
else
siteMapPriority = "0.5";
// Write entry for the post
xmlWriter.WriteStartElement("url");
xmlWriter.WriteElementString("loc",new Macros().FullUrl(post.Url));
xmlWriter.WriteElementString("lastmod", post.ModifiedOn.ToString("yyyy-MM-dd"));
xmlWriter.WriteElementString("priority", siteMapPriority);
xmlWriter.WriteEndElement();
</code>
Thanks again!
@Backup Software Guy
Thank you for the modification. I'll include your change in the next build.

#1
Keith Rull
12.12.2007 @ 2:15 PM
great job @keyvan i was thinking of doing the same thing last night but got caught up with some errands. very nice and simple implementation