tags:

views:

3224

answers:

7

I'm trying to create an automatic sitemap ActionResult that outputs a valid sitemap.xml file. The actual generation of the file is not a problem, but I can't seem to figure out how to populate the list of URL's in the system. Here is the code I have so far:

    public ContentResult Sitemap()
    {
        XNamespace xmlns = "http://www.sitemaps.org/schemas/sitemap/0.9";
        XElement root = new XElement(xmlns + "urlset");

        //some kind of foreach here to get the loc variable for all URLs in the site
        //for each URL in the collection, add it to the root element as here

        //root.Add(
        //    new XElement("url", 
        //        new XElement("loc", "http://google.com"), 
        //        new XElement("changefreq", "daily")));

        using (MemoryStream ms = new MemoryStream())
        {
            using (StreamWriter writer = new StreamWriter(ms, Encoding.UTF8))
            {
                root.Save(writer);
            }

            return Content(Encoding.UTF8.GetString(ms.ToArray()), "text/xml", Encoding.UTF8);
        }
    }

For instance, suppose I have two controllers, and each controller has two actions associated with them:

HelpController

  • Edit
  • Create

AboutController

  • Company
  • Management

I can't seem to figure out how to get a list of URL's like:

+1  A: 

Have you tried something like this:

http://blog.maartenballiauw.be/post/2008/08/29/Building-an-ASPNET-MVC-sitemap-provider-with-security-trimming.aspx

After re-reading your question, I see that you want something a little different than the example I provided. I think you would have to reflect all of the known controllers and their actions to build a sitemap dynamically.

It would be far simpler to use a database or sitemap file as your source I think.

+3  A: 

As likwid mentions, you want to reflect upon your model(s) namespace and obtain all classes that implement IController. Once you have the collection, you want to reflect to see what Members (methods) return the type ActionResult.

Perhaps you can create your own attribute, [SitemapAttribute] that lets you selectively specify what methods to index in the sitemap (i.e., Index(), but not Edit()). Yeah, I like that idea of controlling which methods (urls) gets written.

This is an excellent question because I was just thinking of doing the same. +1!

// Controller abstract implements IController
public class HelpController : Controller
{
  public HelpController()
  {
  }

  [Sitemap]
  public ActionResult Index()
  {
    // does get written to the file, cause of [Sitemap]
  }

  public ActionResult Create()
  {
    // does not get mapped to the file
  }

  public ActionResult Edit()
  {
    // does not get mapped to the file
  }

  [Sitemap]
  public ActionResult ViewArticle()
  {
    // would get indexed.
  }
}

For how to do reflection, here's a good MSDN article to get you introduced to reflection:

http://msdn.microsoft.com/en-us/library/ms172331.aspx

Good question!

eduncan911
A: 

Take a look at the code behind Phil Haack's "Route Debugger":

http://haacked.com/archive/2008/03/13/url-routing-debugger.aspx

defeated
+1  A: 

So, the getting of the controllers and actions seems to me to be the relatively trivial part. The hard part is being able to get all the possible parameter values that you might want to show in the urls of your sitemap. If you have a URL pattern like {controller}/{action}/{id}, then you're not going to be able to determine through reflection what the meaning of id is, or the possible values. The best you can do is determine the system type.

What occurred to me as I was looking at this is that a sitemap is really just another view of your site's data. So one random thought I had was that if you inherit from a base controller in your app, and you have a method on that base controller that has to be implemented, e.g.:

abstract ActionResult SiteMapSnippet();

Then you could create a SiteMapController which calls each of the other controllers in the solution and asks them for their snippet, and then renders them all together in one final view. Sort of a composite controller, though that's not a concept that's been added to this framework yet.

Paul
+3  A: 

I took a look at Maarten Balliauw's approach per likwid's comment, but it seems to be overkill for what I'm trying to do.

I've hacked together a temporary solution. I'm simply passing the controller and action names to generate the URL's. In order to generate the URL's, I'm using the following code:

    List<string> urlList = new List<string>();
    urlList.Add(GetUrl(new { controller = "Help", action = "Edit" }));
    urlList.Add(GetUrl(new { controller = "Help", action = "Create" }));
    urlList.Add(GetUrl(new { controller = "About", action = "Company" }));
    urlList.Add(GetUrl(new { controller = "About", action = "Management" }));

where GetUrl is as below:

    protected string GetUrl(object routeValues)
    {
        RouteValueDictionary values = new RouteValueDictionary(routeValues);
        RequestContext context = new RequestContext(HttpContext, RouteData);

        string url = RouteTable.Routes.GetVirtualPath(context, values).VirtualPath;

        return new Uri(Request.Url, url).AbsoluteUri;
    }

This seems to do the trick for now, though I do like the idea of having actionfilter's applied to certain actions that get pulled together automatically.

dp
+4  A: 

I posted an answer above. But, here a CodePlex project recently released that does Sitemap of MVC sites:

http://mvcsitemap.codeplex.com/

Enjoy!

eduncan911
That's an ASP.NET sitemap not a sitemap.xml file for a search engine
Hightechrider
+2  A: 

Define an ActionFilterAttribute like this to put on any Action method that is an actual page that you want to list in your sitemap:-

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class MVCUrlAttribute : ActionFilterAttribute
{
    public string Url { get; private set; }

    public MVCUrlAttribute(string url)
    {
        this.Url = url;
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        // Put this 'canonical url' into the model (which feeds the view)
        // to help search engines with issues of duplicate content
        filterContext.Controller.ViewData["CanonicalUrl"] = url;
        base.OnResultExecuting(filterContext);
    }
}

Now add something like this to your Global application start code, or use it in your sitemap.xml generating code:-

   // Find all the MVC Routes
    Log.Debug("*** FINDING ALL MVC ROUTES MARKED FOR INCLUSION IN SITEMAP");
    var allControllers = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(Controller)));
    Log.DebugFormat("Found {0} controllers", allControllers.Count());

    foreach (var controllerType in allControllers)
    {
        var allPublicMethodsOnController = controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance);
        Log.DebugFormat("Found {0} public methods on {1}", allPublicMethodsOnController.Count(), controllerType.Name);

        foreach (var publicMethod in allPublicMethodsOnController)
        {
            var mvcurlattr = publicMethod.GetCustomAttributes(true).OfType<MVCUrlAttribute>().FirstOrDefault();
            if (mvcurlattr != null)
            {
                string url = mvcurlattr.Url;
                Log.Debug("Found " + controllerType.Name + "." + publicMethod.Name + " <-- " + url);
                Global.SiteMapUrls.Add(url);  //<-- your code here using url
            }
        }
    }

You can extend the attribute class to perhaps also include the frequency of update hint.

Hightechrider