ASP.Net and metadata
Building great websites have several parts to it. First of all you need a great idea of what to fill the site with, then you’ll need some great technology in the background (sloppy code can achieve lots of great things aswell, but you won’t want to go in there and add things afterwards) and then a great presentation to the users of your site. In this article I will touch two parts of this, the technology (ASP.Net) and the presentation (metadata).
First of all I just want to state that I’m not a believer in all the SEO crap thats out there. I simply believe that if you have a great site that people visit and link to it will rank higher in search engines. A great site though will incorporate lots of things that “SEO experts” are chattering about like validating markup, good titles and things like that but that doesn’t mean it is those parts that put it high in the searchresults.
When working on a new version the CMS I use for CrazyBeaver Softwares website I ran into a really disturbing problem with how ASP.Net handles metadata. Look at the sample below.
<head runat="server">
<title>My website</title>
<meta name="keywords" content="" />
</head>
It looks alright doesn’t it? Well if you browse to that page you’ll see that the generated source is different.
<head>
<title>My website</title>
<meta name="keywords" />
</head>
Where did my content go? Just because it is empty doesn’t mean it can be removed just like that. If you pay a quick visit to www.w3.org and look at the xhtml specification you’ll see that the content attribute is a REQUIRED attribute of the meta element and therefore shouldn’t be removed. Now I hear you say that it shouldn’t matter since it didn’t convey any information anyway and while that is true the removal of that attribute also made sure that the page won’t generate valid html. Sure, it’s a very small error and won’t stop anything that doesn’t require a strict DTD validation to display the page (and no browsers do that since they wouldn’t be able to browse large parts of the web) but start out small and soon you won’t even bother to close tags properly since the page doesn’t validate anyway.
So, this is a behavior of ASP.Net that we need to work around somehow. There are a few choices here. You could simply loop through the HtmlMeta controls during Page_Load and check if they have empty content. It would look something like this.
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
Control[] headerControls = new Control[Page.Header.Controls.Count];
Page.Header.Controls.CopyTo(headerControls, 0);
foreach (Control control in headerControls)
{
HtmlMeta meta = control as HtmlMeta;
if (meta == null)
continue;
if (string.IsNullOrEmpty(meta.Content))
{
Page.Header.Controls.Remove(control);
break;
}
}
}
}
Does this look as a good enough solution for you? It will do the work needed for most parts (it won’t remove a completely empty meta for some reason though so a <meta name=”" content=”" />will still fail your validation..) but it isn’t the nicest solution since you’ll need to loop through all controls in your header for every pageview that isn’t a postback. Also, maybe you want to specify an empty content for some reason since you have some other piece of code or application that relies on them being there. So what would be a nicer solution? Well writing an adapter of course! In ASP.Net 2.0 they introduced a cool thing called control adapters which basicly let you override the rendering of an existing webcontrol. In this case we will use it to force the HtmlMeta control to always render a content attribute.
using System.Web.UI;
using System.Web.UI.Adapters;
using System.Web.UI.HtmlControls;
namespace CrazyBeavers.Web.UI.Adapters
{
public class HtmlMetaAdapter : ControlAdapter
{
private static string[] _i8nAttributes = new string[] { "lang", "xml:lang", "dir" };
protected override void Render(HtmlTextWriter writer)
{
HtmlMeta meta = this.Control as HtmlMeta;
writer.WriteBeginTag("meta");
if (!string.IsNullOrEmpty(meta.HttpEquiv))
writer.WriteAttribute("http-equiv", meta.HttpEquiv);
if (!string.IsNullOrEmpty(meta.Scheme))
writer.WriteAttribute("scheme", meta.Scheme);
if (!string.IsNullOrEmpty(meta.Name))
writer.WriteAttribute("name", meta.Name);
foreach (string attr in meta.Attributes.Keys)
foreach (string i18n in _i8nAttributes)
if (attr.Equals(i18n, System.StringComparison.InvariantCultureIgnoreCase))
writer.WriteAttribute(i18n, meta.Attributes[i18n]);
writer.WriteAttribute("content", meta.Content);
writer.Write(HtmlTextWriter.SelfClosingTagEnd);
}
}
}
Just drop that piece of code in your App_Code or compile it as an assembly and put it in the Bin-folder on your site. There is still one step that needs to be completed for this to work though. All control adapters needs to be referenced via an browser-file in App_Browsers aswell (which is a really cool thing since you can set conditions and use adapters on certain controls for certain browsers, but more on that some other time). Below is a short sample of my Default.browser-file, it doesn’t use any extra features of the browser-file and just say that I want to use my adapter everywhere.
<browsers>
<browser refID="Default">
<controladapters>
<adapter controlType="System.Web.UI.HtmlControls.HtmlMeta"
adapterType="CrazyBeavers.Web.UI.Adapters.HtmlMetaAdapter" />
</controladapters>
</browser>
</browsers>
There you go, now all your MetaCotrol should always render properly for you even though you don’t put any data at all in them. Feel free to comment on this article if you have other solutions or ideas on how to work around this problem.