Building a favicon module for Orchard

(c) Bertrand Le Roy 2011I built a little module for Orchard tonight and it involves a couple of interesting magic tricks so I thought I'd share.

The module is replacing the default Orchard favicon with whatever you configure in site settings:

The default favicon The new favicon
The site settings for favicon

I haven't created any specific storage for the icons and instead decided to use a simple convention for the already existing Media module. What you can see in the site settings in the picture above is a list of suggestions that is just a list of the icons that are found in the favicon subfolder of the Media module. In order to do that, I added a reference to Orchard.Media in my module's csproj and the corresponding dependency in the module's manifest.

That done, I can get a dependency on IMediaService injected into my site settings part driver and get the files' URLs from the favicon subfolder:

faviconSuggestions = new List<string>(
    _mediaService.GetMediaFiles("favicon")
    .Select(f =>
_mediaService.GetPublicUrl("favicon/" + f.Name)));

The really tricky part though is to replace the existing favicon with the one that the user configures. The default favicon is introduced by the Document.cshtml template that can be found in the fallback SafeMode theme:

RegisterLink(new LinkEntry {
    Type = "image/x-icon",
    Rel = "shortcut icon",
    Href = Url.Content(
"~/modules/orchard.themes/Content/orchard.ico")});

The registered link then gets rendered a little farther in the same template:

<head> 
    <meta charset="utf-8" /> 
    <title>@Html.Title()</title> 
     @Display(Model.Head)
</head>

This code defines a "Head" zone in the current shape, which happens to be Layout, for which Document is just a wrapper. Don't worry if you're a little lost, that's not really the important part, I'm only explaining how I tracked down where I needed to act.

This "Head" zone is populated by the ResourceFilter as follows:

head.Add(_shapeFactory.Metas());
head.Add(_shapeFactory.HeadLinks());
head.Add(_shapeFactory.StylesheetLinks());
head.Add(_shapeFactory.HeadScripts());

It should become clear from this that the right shape to modify here is HeadLinks. We just need an event that is triggered right before that shape gets rendered, the "Displaying" event. We can do that from a shape table provider:

public class FaviconShapes : IShapeTableProvider {
    public void Discover(ShapeTableBuilder builder) {
        builder.Describe("HeadLinks")
            .OnDisplaying(shapeDisplayingContext => {
                // Do stuff here.
            });
    }
}

Now all we need to do is get the current list of links and get any favicon from it:

var resourceManager = workContext.Resolve<IResourceManager>();
var links = resourceManager.GetRegisteredLinks();
var currentFavicon = links
    .Where(l => l.Rel == "shortcut icon" &&
l.Type == "image/x-icon") .FirstOrDefault();

And then if we found one we can just replace its Href property, or if there isn't one already we create it:

// Modify if found
if (currentFavicon != default(LinkEntry)) {
    currentFavicon.Href = faviconUrl;
}
else {
    // Add the new one
    resourceManager.RegisterLink(new LinkEntry {
        Type = "image/x-icon",
        Rel = "shortcut icon",
        Href = faviconUrl
    });
}

And that's it. Share and enjoy!

The module (with full source code) can be downloaded from the Orchard Gallery:
http://orchardproject.net/gallery/Packages/Modules/Details/Vandelay-Favicon-1-0

22 Comments

  • Wow :-) It's a very great example of how modular Orchard is!

    (And from this very simple example I get how hard to document such extensibility points :-))

  • A nice feature i'll be sure to use some day. I'm glad to see it in the galery instead of a NuGet package which i always open with my favorite zip tool.

  • Nice module!
    How about giving a detailed instruction on how to "bundle" this module into a Orchard Azure package? Can't just get it to work :)

  • @Wictor: did you read this? http://orchardproject.net/docs/Deploying-Orchard-to-Windows-Azure.ashx#Deploying_Orchard_to_Azure_with_optional_modules_2

    What exactly doesn't work? Feel free to e-mail me at bleroy at you know where.

  • What about seo ?

    Wordpress sucks on SEO so to kill wordpress with orchard it needs ace seo.

  • @Jason: what about it? People put many different sets of expectations under SEO. What's yours?
    I published a meta description and keywords module there: http://orchardproject.net/gallery/Packages/Modules/SEO
    Nowadays, SEO is above all about offering relevant contents in clean markup, under friendly URLs. And have other well referenced sites link to you. There is only so much the platform can do for you.

  • Hey Bertrand: nice module, but unfortunately I can't enable it :( It installed fine from the gallery but when I enable in Configuration|Features, I get the following server error:

    Cannot insert the value NULL into column 'Id', table '[database name]__Settings_ShellFeatureRecord'; column does not allow nulls. INSERT fails.
    The statement has been terminated.

    Any ideas?

  • @Russ: none at all. Can you install other modules fine?

  • @Bertrand: D'oh! it appears it's an issue to install any modules. I copied the database via an SSIS package to a provider, so I'll try again by installing from a backup.

    Many Thanks

  • That seems like a lot of work to add a favicon...I mean all of that work to add something like this to a page:



    It has the smell of an over-engineered abstraction.

  • @Jeff: you can go ahead and do that in the document.cshtml of your theme and you'd be done. This is in response to several users who wanted more than that: admin UI to change the icon and a solution that works across all themes without requiring the change of a template file.

  • @Bertrand - I gotcha programmer free maintenance. I stand corrected :)

  • @Jeff: oh, and multi-tenancy is another reason to use this: you want each tenant to be able to choose their icon.

  • Hello Bertrand,

    I'm trying to use your FavIcon module in a multi tenant orchard site. It works in the primary tenant, but not in the other tenant. I have enabled it for the sub tenant as well, but it does not show up in the settings page of the tenant. Do you have any ideas why it fails?

  • @Mathias: weird. I've filed a bug to track this: http://orchard.codeplex.com/workitem/17558

  • This one worked for me. Thanks Bertrand!

  • Hi Bertrand,

    I have tried adding code like this to a theme - I added a class like your FaviconShapes class but the Discover method never gets called - what lets the Core know to call my IShapeTableProvider?

    Thanks!
    Sean

  • @Sean: That's because you need a project in your theme for code to get picked up.

  • I do have my theme in a project. Is that all, if I just have the theme in a project and a class that implements IShapeTableProvider then it should just work?

  • @Sean: to be clear, it's not that you need your theme in a project, it's that your theme must have a csproj file with the same name as its folder. Then if that project file references cs files, they will get compiled. So yes.

  • Would it be possible to have a route for
    /favicon.ico

    For old browsers that assume it'll be in the route? Getting a few 404's for them looking for that file. I'm currently adding a url rewrite.

  • @Sarkie: please file a bug for that on http://vandelay.codeplex.com/WorkItem/Create

Comments have been disabled for this content.