ASP.NET WebForms SEO: Moving View State to Bottom of the Form

As we all know, SEO is very important for almost all sites and some of us are still stuck with sites based on ASP.NET WebForm. But there are still things you can do to optimize these sites.

Since Google and other search-engines only indexes X amount of KB of a page, you don't want half of that content to be meaningless View State (which you should TRULY understand by now).

One solution is to compress the View State, but in this article I will show you how to move it to the bottom of the Form-tag. To achieve the latter, I will use a HttpModule, which will intercept all requests to files served through ASP.NET in the IIS.

First we need to hook in to the BeginRequest-event to only intercept requests to .aspx-pages.

public class ViewStateSeoHttpModule : IHttpModule {
    public void Init(HttpApplication context) {
        context.BeginRequest += new EventHandler(BeginRequest);
    }

    private void BeginRequest(object sender, EventArgs e) {
        HttpApplication application = sender as HttpApplication;

        bool isAspNetPageRequest = GetIsAspNetPageRequest(application);
        if(isAspNetPageRequest) {
            application.Context.Response.Filter =
                new ViewStateSeoFilter(application.Context.Response.Filter);
        }
    }

    private bool GetIsAspNetPageRequest(HttpApplication application) {
        string requestInfo = application.Context.Request.Url.Segments.Last();
        bool isAspNetPageRequest = requestInfo.ToLowerInvariant().Contains(".aspx");
        return isAspNetPageRequest;
    }
    // [...]

What we do here is add a Reponse Filter which moves the View State to the bottom of the Form. I made this to an internal class called ViewStateSeoFilter.

internal class ViewStateSeoFilter : Stream {
    private string _topPageHiddenFields;
    private Stream _originalFilter;

    public ViewStateSeoFilter(Stream originalFilter) {
        _originalFilter = originalFilter;
    }

    public override bool CanRead { get { return true; } }
    public override bool CanSeek { get { return true; } }
    public override bool CanWrite { get { return true; } }
    public override long Length { get { return 0; } }
    public override long Position { get; set; }

    public override void Flush() {
        _originalFilter.Flush();
    }

    public override int Read(byte[] buffer, int offset, int count) {
        return _originalFilter.Read(buffer, offset, count);
    }

    public override long Seek(long offset, SeekOrigin origin) {
        return _originalFilter.Seek(offset, origin);
    }

    public override void SetLength(long value) {
        _originalFilter.SetLength(value);
    }
    public override void Close() {
        _originalFilter.Close();
    }

    public override void Write(byte[] buffer, int offset, int count) {
        byte[] data = new byte[count];
        Buffer.BlockCopy(buffer, offset, data, 0, count);
        string html = Encoding.Default.GetString(buffer);

        html = ExtractPageTopHiddenFields(html);
        html = InsertExtractedHiddenFields(html);

        byte[] outdata = Encoding.Default.GetBytes(html);
        _originalFilter.Write(outdata, 0, outdata.GetLength(0));
    }

    private string ExtractPageTopHiddenFields(string html) {
        int formStartIndex = html.IndexOf("<form");
        if(formStartIndex < 0) {
            return html;
        }

        int divStartOpenIndex = html.IndexOf("<div", formStartIndex);
        if(divStartOpenIndex < 0) {
            return html;
        }
        int divStartCloseIndex = html.IndexOf(">", divStartOpenIndex);
        int divStartLenght = divStartCloseIndex - divStartOpenIndex;
        int divEndOpenIndex = html.IndexOf("</div", divStartOpenIndex);
        int divEndCloseIndex = html.IndexOf(">", divEndOpenIndex);

        int divContentLength = divEndOpenIndex - divStartCloseIndex - 1;

        _topPageHiddenFields = html.Substring(divStartCloseIndex + 1, divContentLength);
        html = html.Remove(divStartOpenIndex, divEndCloseIndex - divStartOpenIndex + 1);
        return html;
    }

    private string InsertExtractedHiddenFields(string html) {
        if(string.IsNullOrEmpty(_topPageHiddenFields)) {
            return html;
        }

        int insertIndex = html.IndexOf("</form>");
        if(insertIndex > 0) {
            html = html.Insert(insertIndex, _topPageHiddenFields + Environment.NewLine);
            _topPageHiddenFields = null;
        }

        return html;
    }
}

One bonus is that it moves all available Hidden Input HTML-tags controlled by ASP.NET to the bottom of the Form-tag. It seems to skip the Hidden Input that handles EventValidation, which is a good thing, because ASP.NET seems to want this tag early in the Page Life Cycle.

Comments