SEO Friendly Paging with ASP.NET 2.0 Data Controls


By Peter Bromberg
Printer Friendly Version
  

ASP.NET has lots of "out of the box" features that make the display of data easy, including pageable GridViews and DataGrids with little or no code. But the stock paging mechanism uses javascript to cause the postback, and the url of the new "page" doesn't change. This is not "SEO friendly", because the Googlebot and other search engine crawlers click on the links, and see the result as a page they've already requested, so it doesn't get indexed.



From an SEO (Search Engine Optimization) standpoint, having your pageable display passed up by search bots is not good. Here is a way to handle paging with Data Controls such as the DataList - which doesn't offer native paging -- and ensure that each new page request is a unique url that the search engines will slurp up and index.

 For our sample data, we will cheat a little and use a custom RSS search to google blog search, returning the results as RSS which can be loaded directly into a DataSet via the ReadXml method. We will also cache the DataSource in Session so that we do not have to make a separate webrequest each time we request the next page of results. We will use the PagedDataSource class to handle the paging for the DataList. PagedDataSource encapsulates the paging-related properties of a data-bound control (such as DataGrid, GridView, DetailsView, FormView, Repeater or DataList) that allow it to perform paging. By using the PagedDataSource as the datasource for one of these controls, you can easily enable paging features even if the control does not support paging on its own.

 PagedDataSource uses the best available method to enumerate over the data belonging to the current page. If the underlying data source supports indexed access (such as System.Array and System.Collections.IList), this class uses it. Otherwise, it uses the enumerator created by the GetEnumerator method. You can use the PagedDataSource with a DataTable (which does not support IEnumerable or IList) by simply binding to its DefaultView (DataView) -- which does support this.

 PagedDataSource offers all the members that you would expect to perform paging - including CustomPaging:

  •   AllowCustomPaging  Gets or sets a value indicating whether custom paging is enabled in a data-bound control.
  •   AllowPaging  Gets or sets a value indicating whether paging is enabled in a data-bound control.
  •   AllowServerPaging  Gets or sets a value indicating whether server-side paging is enabled.
  •   Count  Gets the number of items to be used from the data source. 
  •   CurrentPageIndex  Gets or sets the index of the current page. 
  •   DataSource  Gets or sets the data source.
  •   DataSourceCount  Gets the number of items in the data source.
  •   FirstIndexInPage  Gets the index of the first record displayed on the page.
  •   IsCustomPagingEnabled  Gets a value indicating whether custom paging is enabled.
  •   IsFirstPage  Gets a value indicating whether the current page is the first page.
  •   IsLastPage  Gets a value indicating whether the current page is the last page.
  •   IsPagingEnabled  Gets a value indicating whether paging is enabled.
  •   IsReadOnly  Gets a value indicating whether the data source is read-only.
  •   IsServerPagingEnabled  Gets a value indicating whether server-side paging support is enabled.
  •   IsSynchronized  Gets a value indicating whether access to the data source is synchronized (thread-safe).
  •   PageCount  Gets the total number of pages necessary to display all items in the data source.
  •   PageSize  Gets or sets the number of items to display on a single page.
  •   SyncRoot  Gets the object that can be used to synchronize access to the collection.
  •   VirtualCount  Gets or sets the virtual number of items in the data source when custom paging is used.

A page with a DataList needs no special items on it except for  2 LinkButtons that provide the  <  and  > functionality to move forward or backward one "page". What's important is the codebehind, which implements the PagedDataSource:

using System;
using System.Data;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace SEOFriendlyPaging
{
    public partial class _Default : Page
    {
        protected PagedDataSource pagedData = null;
        protected string srchTerm = "ASP.NET"; // use a default search term

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                // if initial page load---
                pagedData = new PagedDataSource();
            }
            else
            {
                pagedData = (PagedDataSource) Session[srchTerm];
            }
            BindPaged();
        }

        public void Prev_Click(object obj, EventArgs e)
        {
            int newPageIndex = ((int) (pagedData.CurrentPageIndex - 1));
            pagedData.CurrentPageIndex = newPageIndex;
            Response.Redirect(Request.CurrentExecutionFilePath + "?Page=" +
                              newPageIndex.ToString());
        }

        public void Next_Click(object obj, EventArgs e)
        {
            int newPageIndex = ((int) (pagedData.CurrentPageIndex + 1));
            pagedData.CurrentPageIndex = newPageIndex;
            Response.Redirect(Request.CurrentExecutionFilePath + "?Page=" +
                              newPageIndex.ToString());
        }


        private void BindPaged()
        {
            if (txtSearch.Text != "")
                srchTerm = txtSearch.Text;
            DataView dv = null;
            if (Session[srchTerm] == null)
            {
                string baseUrl = "http://blogsearch.google.com/blogsearch_feeds?hl=en&q=";
                string baseUrlEnd = "&num=100&output=rss";
                if (srchTerm == "") srchTerm = "ASP.NET";
                string fullSearchUrl = baseUrl + srchTerm + baseUrlEnd;
                DataSet ds = new DataSet();
                ds.ReadXml(fullSearchUrl);
                dv = ds.Tables[2].Copy().DefaultView;
                Session[srchTerm] = pagedData;
            }
            else
            {
                pagedData = (PagedDataSource) Session[srchTerm];
                dv = (DataView) pagedData.DataSource;
            }
            pagedData.DataSource = dv;
            pagedData.AllowPaging = true;
            pagedData.PageSize = 5;
            if (Request.QueryString["Page"] == null)
                pagedData.CurrentPageIndex = 0;
            else
            {
                int pg = int.Parse(Request.QueryString["Page"]);
                if (pg < 0) pg = 0;
                pagedData.CurrentPageIndex = pg;
            }
            btnPrev.Visible = (!pagedData.IsFirstPage);
            btnNext.Visible = (!pagedData.IsLastPage);
            dlSEOFriendly.DataSource = pagedData;
            dlSEOFriendly.DataBind();
        }

        protected void btnSearch_Click(object sender, EventArgs e)
        {
            BindPaged();
        }
    }
}
You can also use a static class that will generate a set of numbered links as a string of HTML, which you can then assign to a Literal or similar control. This is what I use in the sample you can download at the bottom of this article:

using System;
using System.Text;
using System.Web.UI.WebControls;

namespace SEOFriendlyPaging
{
    public static class Pager
    {
        public static string CreatePagerLinks(PagedDataSource pgdDataSource, string BaseUrl)
        {
            StringBuilder sbPager = new StringBuilder();
            //sbPager.Append("More: ");
            if (!pgdDataSource.IsFirstPage)
            {
                // first page link
                sbPager.Append("<a href=\"");
                sbPager.Append(BaseUrl);
                sbPager.Append("\">|<</a> ");
                if (pgdDataSource.CurrentPageIndex != 1)
                {
                    // previous page link
                    sbPager.Append("<a href=\"");
                    sbPager.Append(BaseUrl);
                    sbPager.Append("&page=");
                    sbPager.Append(pgdDataSource.CurrentPageIndex.ToString());
                    sbPager.Append("\" alt=\"Previous Page\"><<</a>  ");
                }
            }
            // calc low and high limits for numeric links
            int intLow = pgdDataSource.CurrentPageIndex - 1;
            int intHigh = pgdDataSource.CurrentPageIndex + 3;
            if (intLow < 1) intLow = 1;
            if (intHigh > pgdDataSource.PageCount) intHigh = pgdDataSource.PageCount;
            if (intHigh - intLow < 5) while ((intHigh < intLow + 4) && intHigh < pgdDataSource.PageCount) intHigh++;
            if (intHigh - intLow < 5) while ((intLow > intHigh - 4) && intLow > 1) intLow--;
            for (int x = intLow; x < intHigh + 1; x++)
            {
                // numeric links
                if (x == pgdDataSource.CurrentPageIndex + 1) sbPager.Append(x.ToString() + "  ");
                else
                {
                    sbPager.Append("<a href=\"");
                    sbPager.Append(BaseUrl);
                    sbPager.Append("&Page=");
                    sbPager.Append(x.ToString());
                    sbPager.Append("\">");
                    sbPager.Append(x.ToString());
                    sbPager.Append("</a>  ");
                }
            }
            if (!pgdDataSource.IsLastPage)
            {
                if ((pgdDataSource.CurrentPageIndex + 2) != pgdDataSource.PageCount)
                {
                    // next page link
                    sbPager.Append("<a href=\"");
                    sbPager.Append(BaseUrl);
                    sbPager.Append("&Page=");
                    sbPager.Append(Convert.ToString(pgdDataSource.CurrentPageIndex + 2));
                    sbPager.Append("\">>></a>  ");
                }
                // last page link
                sbPager.Append("<a href=\"");
                sbPager.Append(BaseUrl);
                sbPager.Append("&Page=");
                sbPager.Append((pgdDataSource.PageCount - 1).ToString());
                sbPager.Append("\">>|</a>");
            }
            // convert the final links to a string and  return for assignment
            return sbPager.ToString();
        }
    }
}
The downloadable Zip solution includes a version using the above numbered pager class, and I have even included the paging into the Header and Footer of the DataList.
This is far from a complete "production ready" implementation -- I threw it together quickly -- but it illustrates the basic technique of how to use the PagedDataSource to "take over" the function of paging for a control that does not natively support it.

Most importantly, each request for a "next" or "previous" page represents a unique url and querystring. And that means that when the bots click, they'll index it, unlike as with the built-in paging you have with ASP.NET data controls, which aren't "SEO - Friendly".
According to the latest information from Matt Cutts, Google's "SEO" guru, Google does not penalize urls that have querystring parameters - as long as there aren't more than two or three - contrary to all the "Hype Science" of the Url Rewriting crowd.

Remember: When you are developing a web site with a view toward how to make it Search Engine Friendly, the ONLY THING the search engine bots know how to do -- is to follow links. They cannot deal with GridView Paging Javascript. They cannot deal with Flash. They ONLY know how to follow links and look at the anchor text before they go to the link, make sure it is "new" (not already indexed) and "slurp" it.

You can download the Visual Studio 2005 Solution and experiment with the technique. No database setup is needed, since we are getting the datasource as a DataSet result from a Google Blog search -- so there is no setup to worry about.


Biography
Peter Bromberg is a C# MVP, MCP, and .NET expert who has worked in banking ,financial and telephony for 20 years. Pete focuses exclusively on the .NET Platform, and his samples at GotDotNet.com have been downloaded over 56,000 times. Peter enjoys producing 3D raytraced digital photo collage with Maya, the beach, and fine wines. You can view Peter's UnBlogIttyUrl, and BlogMetafinder sites.
Please post questions at forums, not via email!

button

 
Article Discussion: SEO Friendly Paging with ASP.NET 2.0 Data Controls
Peter Bromberg posted at 21-Aug-07 06:45
Original Article

 
Multiple Grids on 1 page (SEO Friendly)
Yen Dutt Jain replied to Peter Bromberg at 09-Oct-07 03:24

This article looks good. Can we have it tweaked to work for multiple grids by doing something like Grid1Page=1&Grid2Page=2 and on hyperlinks to persist both the Grid Page Indexes?


 
Tweaking it.
Peter Bromberg replied to Yen Dutt Jain at 22-Mar-08 09:18
I gave you the source code, its in the public domain. If you want to tweak it that would be a good programming exercise for you.