I'm Keyvan Nayyeri, a 25 years old Ph.D. student at
the Computer Science department of
the University of Texas at San Antonio.
I'm also
a Software Architect and Developer and previously held a B.Sc.
degree in Applied Mathematics.
This is my blog where I publish content about various topics specifically Programming Languages and Compilers, Software
Engineering and Programming.
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.
Keith Rull
Dec 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
gigi
Feb 08, 2008 4:07 PM
#
i have enabled your plugin but in logs i get an error.
can i send log on your email?
Tiernan OToole
Jun 04, 2008 8:19 AM
#
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
Keyvan Nayyeri
Jun 04, 2008 8:23 AM
#
@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).
Tiernan OToole
Jun 04, 2008 9:26 AM
#
@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! :)
Keyvan Nayyeri
Jun 04, 2008 9:32 AM
#
@Tiernan:
It would be easy but if you had any issues, just ping me and I'll send you a quick update that works.
Binu
Jul 22, 2008 11:13 PM
#
Nice Implementation, i would like to implement on my site.
Backup Software Guy
Oct 31, 2008 10:02 AM
#
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!
Keyvan Nayyeri
Oct 31, 2008 10:23 AM
#
@Backup Software Guy
Thank you for the modification. I'll include your change in the next build.
The Friend of a Friend Plug-In for Graffiti
Jan 26, 2009 6:51 AM
#
The era of Web 3.0 has begun and there is a fast transition to Web 3.0 styles and definitions. The recent economy crisis can slow the progress down to a reasonable extent because the nature of Web 3.0 requires good investments by bigger companies in creation
Ryan
Oct 24, 2009 3:40 PM
#
It appears you're pretty knowledgable with the grafitti google sitemap plugin. Any chance you could take a few moments to tell me why this error is coming up...
System.NullReferenceException: Object reference not set to an instance of an object.
at Graffiti.Core.Macros.FullUrl(String absoluteUrl)
at GraffitiExtras.Plugins.Sitemap.Sitemap.AddSiteMapToRobots()
Thanks for the help,
Ryan.
James
Nov 20, 2009 10:46 AM
#
Hi,
Thanks for the awesome tool. Any Update on the Error listed above? I get the same issue.
Any help would be awesome.
Thanks
James
Keyvan Nayyeri
Nov 20, 2009 12:29 PM
#
@James
Check this CodePlex project that I had started to release several plug-ins and extensions including this one:
http://graffitiextras.codeplex.com
Leave a Comment