The views expressed on this weblog are mine alone and do not necessarily reflect the views of my employer, Avanade.

« Snip-It Pro Released | Main | Published in the February Issue of MSDN Magazine »

Using the Delegate Control to Add Meta Tags to SharePoint Pages

It has been a while since I last blogged about SharePoint. I have been busy working on other things in the little free time that I have. I collaborated on an article that is in the February edition of MSDN magazine. I also have been working on getting Snip-It Pro, a manager for snippets, ready for a beta at the end of the month.

Anyway, I came up with a cool and easy way of getting Meta tags into SharePoint pages that are part of a document library. Meta tags are html tags that appear in the head of a html page and are used a lot with Search Engine Optimization also known as SEO.

Without this feature, you are basically stuck adding meta information using SharePoint designer, either by adding a set of global tags to the master page or over-ridding the “PlaceHolderAdditionalPageHead” content placeholder on each page. There is no easy way to modify the meta tags without SharePoint Designer.

If you look at the actual master page, you will notice there is a Delegate control in the head element right below the “PlaceHolderAdditionalPageHead.” For those who do not know, a Delegate control is a very cool SharePoint control that allows you to inject or replace the contents at run time using a SharePoint feature.

Leveraging this Delegate Control, I built a custom sharepoint feature that will inject several different types of meta tags into document library pages that have specific columns defined. This enables content managers to edit the properties of document and define any of the following meta tags:

  • Meta Keywords
  • Meta Description
  • Meta Author
  • Meta Copyright
  • Meta Robots
  • Meta Expiration
  • Meta Refresh

The code is actually pretty simple. It’s a custom web control that looks to see if the file is an item with fields that match any of the above names:

public class MetaTagger : WebControl
  private const string KeywordKey = "Meta Keywords";
  private const string DescriptionKey = "Meta Description";
  private const string AuthorKey = "Meta Author";
  private const string RobotsKey = "Meta Robots";
  private const string CopyrightKey = "Meta Copyright";
  private const string ExpirationKey = "Meta Expiration";
  private const string RefreshKey = "Meta Refresh";

  protected override void Render(System.Web.UI.HtmlTextWriter writer)
    if (SPContext.Current != null)
      SPFile file = SPContext.Current.File;
      if (file.Item != null)
        if (file.Item.Fields.ContainsField(KeywordKey) && 
          file.Item[KeywordKey] != null && 
          file.Item[KeywordKey].ToString() != string.Empty)
          writer.Write(string.Format(@"<META name=""keywords"" 
          content=""{0}"">", file.Item[KeywordKey].ToString()));

        if (file.Item.Fields.ContainsField(DescriptionKey) && 
          file.Item[DescriptionKey] != null && 
          file.Item[DescriptionKey].ToString() != string.Empty)
          writer.Write(string.Format(@"<META name=""description"" 
          content=""{0}"">", file.Item[DescriptionKey].ToString()));
        if (file.Item.Fields.ContainsField(AuthorKey) && 
          file.Item[AuthorKey] != null && 
          file.Item[AuthorKey].ToString() != string.Empty)
          writer.Write(string.Format(@"<META name=""author"" 
          content=""{0}"">", file.Item[AuthorKey].ToString()));
        if (file.Item.Fields.ContainsField(RobotsKey) && 
          file.Item[RobotsKey] != null && 
          file.Item[RobotsKey].ToString() != string.Empty)
          writer.Write(string.Format(@"<META name=""robots"" 
          content=""{0}"">", file.Item[RobotsKey].ToString()));
        if (file.Item.Fields.ContainsField(CopyrightKey) && 
          file.Item[CopyrightKey] != null && 
          file.Item[CopyrightKey].ToString() != string.Empty)
          writer.Write(string.Format(@"<META name=""copyright"" 
          content=""{0}"">", file.Item[CopyrightKey].ToString()));
        if (file.Item.Fields.ContainsField(ExpirationKey) && 
          file.Item[ExpirationKey] != null &&
          file.Item[ExpirationKey].ToString() != string.Empty)
          writer.Write(string.Format(@"<META http-equiv=""expires"" 
          content=""{0}"">", file.Item[ExpirationKey].ToString()));
        if (file.Item.Fields.ContainsField(RefreshKey) && 
          file.Item[RefreshKey] != null && 
          file.Item[RefreshKey].ToString() != string.Empty)
          writer.Write(string.Format(@"<META http-equiv=""refresh"" 
          content=""{0}"">", file.Item[RefreshKey].ToString()));

Although the web control is simple enough, the beauty of it is that is injected automatically into the AdditionalPageHead Delegate control which is part of the default Master Page. This is accomplished through a feature. Here is the feature.xml

<?xml version="1.0" encoding="utf-8" ?>
<Feature xmlns="http://schemas.microsoft.com/sharepoint/"
  Title="Meta Tagger"
  Description="Injects Meta Tags into AdditionalPage Head Delegate Control"
    <ElementManifest Location="MetaTaggerSettings.xml"/>

And the following is the content of the MetaTaggerSettings.xml:

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Control Id="AdditionalPageHead" Sequence="50"
    ControlAssembly="MetaTagger, Version=, Culture=neutral,
    PublicKeyToken=576bc1a4564a5293" />

I have packaged everything into a solution package (wsp) file which you can download by clicking here.

Once the feature is activated you will just need to add custom columns to the document libraries for each of the Meta Tag types you want to be able to add using the properties. The column names need to be exactly the same as the one's in the bullet list.

EmailEmail Article to Friend

Reader Comments (54)

Wow, that is the cat's meow.
January 17, 2008 | Unregistered CommenterJuan Carlos Valdez
I noticed the keywords are showing up on the page. Is there anyway to hide/get rid of those?
February 5, 2008 | Unregistered CommenterJustin
Actually figured it. Hopefully this helps someone else out.

<PublishingWebControls:EditModePanel runat=server id="EditModePanel1" PageDisplayMode="Edit">
<asp:ContentPlaceHolder ID="PlaceHolderMetaTags" runat="server" />

You have to encase your ContentPlaceHolder inside an "EditModePanel" control.

February 5, 2008 | Unregistered CommenterJustin
I'm not sure what you were talking about. This appoach leveraged the DelegateControl with Id of AdditionalPageHead.

Either way, I am glad you solved your issue.
February 5, 2008 | Registered CommenterDavid San Filippo
This is great - worked right out of the box - thanks.

1 question - how do I get content in <title></title>
April 4, 2008 | Unregistered CommenterSharon

Just posted about <title> and forgot email address - duh!

Please could you tell me if this can support the <title> field or if not how I can change for optimisation please

Really appreciate your stuff and how easily and sweet this solution works.

April 4, 2008 | Unregistered CommenterSharon
Hi Sharon,

You could easily modify this solution to support outputing a title tag, but you would need to make sure that there was no title tag in your master page, otherwise you would have two of them on your page.

You would need to modify the code as follows:
Add a constant for the key to the field in your document library that will hold the title field. For example:

private const string TitleKey = "Page Title";

then you add another conditional to the render method.

if (file.Item.Fields.ContainsField(TitleKey) &&
file.Item[TitleKey] != null &&
file.Item[TitleKey].ToString() != string.Empty)
writer.Write(string.Format(@"<title>{0}</title>", file.Item[TitleKey].ToString()));

I would probably put this first in the list, because I think I heard having the title appear first is better for SEO. You may even move the delegate control right under the head element as well in your master page.

Anyway good luck.
April 4, 2008 | Registered CommenterDavid San Filippo
I implemented this on our internet facing MOSS site and it works great!
May 1, 2008 | Unregistered CommenterKerry

Thanks for your feedback on my title, much appreciated I'll pass this to someone who codes and get it implmented. Sorry it took me a while to check back but the project got....complicated!

Anyway, I wanted to ask if you came accross teh 302 problem on Google, as someone who obviously has sites that rank in Google. I cannot get google to index the site I'm working on (in Author URL) becasue fo the 302. I had this on another site and solved by taking out browser detects but as far as I know SP isn't doing any...I realise it does redirect but I don't see anyone really struggling with this so I must be doing something wrong (my theory on SP is people arn't complaining then you are inventing a problem!!)

HELP!!!!!!!!!!!! loosing hair daily as the site is live and now less visible than its previous html incarnation!

May 12, 2008 | Unregistered CommenterSharon

I solved the 302 by the way, realised it was my developer doing some fancy authentication!

Anyway, any chace you could do the title change for me and repackage, this has been on the developers list for some time and I'm still no-where near the top :(

Any help much appreciated

July 6, 2008 | Unregistered CommenterSharon
Great Post. I need to do a Title tag also, could you zip your solution that I may extend it to include that piece of work, promise to post my results back :-)
September 19, 2008 | Unregistered CommenterFabian

I am new with regards to being a developer. I'm currently updating my code for the SEO of our website and I'm currently going nowhere now:( I have added the control and the xml needed for the solution but my problem now is that after creating the publishing site columns, they are nowhere to be found on the page. I have created three columns that will handle SEO Title, SEO Keywords and SEO Description. Here is the code I have put on my master page:

<asp:ContentPlaceHolder id="AdditionalPageHead" runat="server">
<SharePoint:DelegateControl runat="server" ControlId="AdditionalPageHead" />

How can I make the publishing columns appear on the page layouts. Any help will be HIGHLY appreciated!

September 26, 2008 | Unregistered CommenterJames
Great help ! much appreciated
February 13, 2009 | Unregistered CommenterAMAR DAVE
Thank you for posting this solution. It was exactly what I was looking for.
March 4, 2009 | Unregistered CommenterPeter Lindeman
Would really love to see s screenshot of this.

When you say that it uses the AdditionalPageHeadPlaceholder, does it overwrite any custom content that might already exist within a Layout Page, or does it just augment what's alerady there?

I'd like to modify this to manage custom CSS and javascripts on a per page basis. My only concern is how to lock it down to prevent tampering by any old content author.

Could you just apply a permission level parameter directly to the delegate control tag?
March 15, 2009 | Unregistered Commenterpanoone
There's really nothing to take a screenshot of. The html is all meta tags and editing them in a doc library is just like editing any other list item.

You probably could customize the control to add a specific css or javaScript, by loading properties of them from doc lib list item, the same way we do for meta tags. You could probably add additional security to control who modifies those fields on the list item. No need to try to secure the control itself as it just reads properties off of the document library.
March 16, 2009 | Registered CommenterDavid San Filippo
Thansk David,

To be more clear, I would like to modify your example to add a new field to my publishing pages that allowed more general head content to be inserted - per page CSS, javascript, metadata etc.

I'd like the content author to see this field (in Layout Page and Page Properties) when creating or modifying a page (I suppose that's the screenshot I was after).

I know this could be considered dangerous but it's just an exercise at this stage. I'd be applying custom permission levels to the column later.

If I create a new site column called "CustomHead" and add it to my publishing page content types, will the following modification give me what I need?

if (file.Item.Fields.ContainsField(CustomHead) &&
file.Item[CustomHead] != null &&
file.Item[CustomHead].ToString() != string.Empty)
writer.Write(string.Format({0}, file.Item[CustomHead].ToString()));

What happens if content already exists? Will it be presented as usual and updated after subsequent edits?
March 17, 2009 | Unregistered Commenterpanoone
Also, this example uses System.Web.UI.HtmlTextWriter. Is there a way to modify this at the field level so that it uses a plain text field?
March 17, 2009 | Unregistered Commenterpanoone
The modificaiton you mention will simply output the contents of what you have into the head of the html doc. Keep in mind, that it should be a well understood tag, otherwise it won't be standards compliant. If you want it for end users, you may be better off exposing fields that get fed into attributes of the tag you are building, just as I do for the Meta Tags in the above sample.

If you want a better way to edit the properties of the Document than the standard sharepoint document properties editor, you'll need to create a custom web part to allow editing of those fields, and use security to ensure only content managers can see that web part in edit mode.

I am using the HTMLTextWriter because I chose to handle the Render Method as opposed to implementing this server control as a Composite Control, adding other server controls to the container during CreateChildControls.

If you do want to use Server Controls anyway, you could just instatiate them and call the RenderControl method, passing the HTMLTextWriter to the method to achieve the same thing without maing too many changes. After all that's what the framework does internally to render child contrls into html. The only thing that doing it like that will impact, is that you won't be setup properly for any post back events, and since you're outputing this in the head, there are no real post back events that are applicablle.

Good Luck.
March 17, 2009 | Registered CommenterDavid San Filippo

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
All HTML will be escaped. Hyperlinks will be created for URLs automatically.