65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 HTTP 404 处理程序生成 Google 站点地图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.04/5 (7投票s)

2007年11月24日

CPOL

6分钟阅读

viewsIcon

47337

网站地图使您的网站对搜索引擎更友好。了解如何使用您网站的 HTTP 404 错误处理程序动态生成它们。

引言

在使用 Microsoft Internet Information Server (IIS) 时,当您指定一个页面来处理网站上的 HTTP 404(未找到)错误时,您不必返回 HTTP 404 错误。相反,您可以返回具有 HTTP 200(成功)结果的动态内容。当您想构建一个sitemap.xml文件来增强您网站的搜索引擎性能时,这会很有用。例如。在本文中,我将向您展示我如何为自己的博客做到这一点。

背景介绍

在使用 IIS 和 ASP.NET 时,有兩種方式可以调用 404 错误处理程序页面。对于已注册的 ASP.NET 页面类型(例如 ASPX、ASMX),您web.config文件中<system.web>部分中的<customError>元素决定了在发生不同类型的错误时将调用哪个页面。对于 404 错误,ASP.NET 会通过 HTTP 302(重定向)重定向来切换到处理程序页面。当您希望在客户端不知情的情况下干净、透明地切换到处理程序页面时,这是不可接受的。然而,当 IIS 而不是 ASP.NET 处理 404 错误时,它的作用类似于内部的Server.Transfer()调用,这意味着客户端不会被重定向。这很好,这正是我们实现动态生成的sitemap.xml文件所需要的。由于 XML 文件不由 ASP.NET 引擎处理,IIS 将会转移到一个我们选择的 ASP.NET 页面,在那里我们可以做任何我们想做的事情。

Google 对网站地图的使用

Google 和其他搜索引擎使用的网站地图依赖于一个简单的 XML 架构,您可以在此处 [^] 找到。如果您和我一样,理解这种简单模式的最佳方法是查看一个真实、实时的网站地图。将我个人博客的实时sitemap.xml文件 [^] 加载到新的 Web 浏览器窗口中,以查看示例。它非常容易理解,您不觉得吗?网站地图是对您网站上robots.txt文件的良好补充,因为它们允许您指定应被搜索引擎索引的内容,而不是不应被索引的内容。当您的网站地图准备好时,请使用Google 网站管理员工具 [^] 注册您的网站地图。

404 处理程序页面代码

当然,使用 404 错误处理程序页面动态生成sitemap.xml文件的关键在于,sitemap.xml文件在您的站点上必须不存在。首先创建一个新的 ASPX 页面来完成这项工作。请记住,该页面可能会双重使用,既生成您的sitemap.xml文件,又处理真实的“未找到”问题。所以,它的样式应该与您的网站设计相匹配。

在我的gotnet.biz网站上,我将我想让 Google 索引的页面引用存储在数据库中。要构建一个动态的网站地图,我所要做的就是根据sitemap.xml规范,将每个页面添加为一个<url>节点。下面是一个名为AddUrlNodeToUrlSet的辅助方法,它将执行此操作。在第一部分中,AddUrlNodeToUrlSet方法显示在一对两部分的偏类中的第一部分。

using System;
using System.Web;
using System.Web.UI;
using System.Xml;

public partial class ErrorNotFound404 : Page
{
    // the standard schema namespace and change frequencies

    // for site maps defined at http://www.sitemaps.org/protocol.php

    private static readonly string xmlns =
        "http://www.sitemaps.org/schemas/sitemap/0.9";
    private enum freq { hourly, daily, weekly, monthly, yearly, never }

    // add a url node to the specified XML document with standard

    // priority to the urlset at the document root

    private static void AddUrlNodeToUrlSet( Uri reqUrl, XmlDocument doc,
        string loc, DateTime? lastmod, freq? changefreq )
    {
        // sanity checks

        if (reqUrl == null || doc == null || loc == null)
            return;

        // call the overload with standard priority

        AddUrlNodeToUrlSet( reqUrl, doc, loc, lastmod, changefreq, null );
    }

    // add a url node to the specified XML document with variable

    // priority to the urlset at the document root

    private static void AddUrlNodeToUrlSet( Uri reqUrl, XmlDocument doc,
        string loc, DateTime? lastmod, freq? changefreq, float? priority )
    {
        // sanity checks

        if (reqUrl == null || doc == null || loc == null)
            return;

        // create the child url element

        XmlNode urlNode = doc.CreateElement( "url", xmlns );

        // format the URL based on the site settings and then escape it

        // ESCAPED( SCHEME + AUTHORITY + VIRTUAL PATH + FILENAME )

        string url = String.Format( "{0}://{1}{2}", reqUrl.Scheme,
            reqUrl.Authority, VirtualPathUtility.ToAbsolute(
            String.Format( "~/{0}", loc ) ) ).Replace( "&", "&" )
            .Replace( "'", "&apos;" ).Replace( "''", "&quot;" )
            .Replace( "<", "&lt;" ).Replace( ">", "&gt;" );

        // set up the loc node containing the URL and add it

        XmlNode newNode = doc.CreateElement( "loc", xmlns );
        newNode.InnerText = url;
        urlNode.AppendChild( newNode );

        // set up the lastmod node (if it should exist) and add it

        if (lastmod != null)
        {
            newNode = doc.CreateElement( "lastmod", xmlns );
            newNode.InnerText = lastmod.Value.ToString( "yyyy-MM-dd" );
            urlNode.AppendChild( newNode );
        }

        // set up the changefreq node (if it should exist) and add it

        if (changefreq != null)
        {
            newNode = doc.CreateElement( "changefreq", xmlns );
            newNode.InnerText = changefreq.Value.ToString();
            urlNode.AppendChild( newNode );
        }

        // set up the priority node (if it should exist) and add it

        if (priority != null)
        {
            newNode = doc.CreateElement( "priority", xmlns );
            newNode.InnerText =
                (priority.Value < 0.0f || priority.Value > 1.0f)
                ? "0.5" : priority.Value.ToString( "0.0" );
            urlNode.AppendChild( newNode );
        }

        // add the new url node to the urlset node

        doc.DocumentElement.AppendChild( urlNode );
    }
}

上面定义的AddUrlNodeToUrlSet方法将在Page_Load事件期间被调用,以构建sitemap.xml文件。它只需为我网站上我想要在网站地图文件中引用的每个页面添加一个<url>节点。请记住,对于我的博客,我是从数据库表中存储的页面名称列表中生成我的网站地图的。所以,在下一部分代码中,当我打开数据库并解析结果时,查找您网站中可搜索页面的代码可能会非常不同。现在让我们看看这个页面第二部分中的Page_Load方法。

using System;
using System.Data.OleDb;
using System.Web;
using System.Web.UI;
using System.Xml;

public partial class ErrorNotFound404 : Page
{
    protected void Page_Load( object sender, EventArgs e )
    {
        string QS = Request.ServerVariables["QUERY_STRING"];

        // was it the sitemap.xml file that was not found?

        if (QS != null && QS.EndsWith( "sitemap.xml" ))
        {
            // build the sitemap.xml file dynamically from add all of the

            // articles from the database, set the MIME type to text/xml

            // and stream the file back to the search engine bot

            XmlDocument doc = new XmlDocument();
            doc.LoadXml( String.Format( "<?xml version=\"1.0\" encoding" +
                "=\"UTF-8\"?><urlset xmlns=\"{0}\"></urlset>", xmlns ) );

            // add the fixed blog URL for this site with top priority

            AddUrlNodeToUrlSet( Request.Url, doc, "MyBlog.aspx", null,
                freq.daily, 1.0f );
            // NOTE: add more fixed urls as necessary for your site

            // this could be done programmatically or better still by

            // dependency injection


            // now query the database and add the virtual URLs for this site

            string connectionString = String.Format(
               "NOTE: set this to suit the needs of your content database" );
            string query = "SELECT PAGE_NAME, POSTING_DATE FROM BLOGDB " +
                "ORDER BY POSTING_DATE";

            OleDbConnection conn = new OleDbConnection( connectionString );
            conn.Open();
            OleDbCommand cmd = new OleDbCommand( query, conn );
            OleDbDataReader rdr = cmd.ExecuteReader();

            if (rdr.HasRows)
            {
                while (rdr.Read())
                {
                    object page_name = rdr[0];
                    object posting_date = rdr[1];
                    if ((object)page_name != null && !(page_name is DBNull))
                    {
                        AddUrlNodeToUrlSet( Request.Url, doc, String.Format(
                            "{0}.ashx", page_name.ToString().Trim() ),
                            (DateTime?)posting_date, freq.monthly );
                    }
                }
            }

            // IMPORTANT - trace has to be disabled or the XML returned will

            // not be valid because the div tag inserted by the tracing code

            // will look like a second root XML node which is invalid

            Page.TraceEnabled = false;

            // IMPORTANT - you must clear the response in case handlers

            // upstream inserted anything into the buffered output already

            Response.Clear();

            // IMPORTANT - set the status to 200 OK, not the 404 Not Found

            // that this page would normally return

            Response.Status = "200 OK";

            // IMPORTANT - set the MIME type to XML

            Response.ContentType = "text/xml";

            // buffer the whole XML document and end the request

            Response.Write( doc.OuterXml );
            Response.End();
        }

        // not the sitemap.xml file so set the standard 404 error code

        Response.Status = "404 Not Found";
    }
}

Page_Load开始时,它会检查QUERY_STRING以查看sitemap.xml文件是否是导致传输发生的那个丢失的文件。这是可能的,因为 IIS 中处理丢失文件名转移的代理会将文件名附加到QUERY_STRING。如果文件名是sitemap.xml,我的代码将启动一个新的 XML 文档,并使用上面显示的AddUrlNodeToUrlSet方法添加虚拟<url>节点。您将在网站地图中包含哪些页面名称完全取决于您网站的内容,因此您必须在我的样本的该区域进行大部分调整。在Page_Load的末尾有一些我想要强调的有趣代码。此时按顺序会发生五件关键事情。

  • 您必须禁用页面跟踪(page tracing);如果它已启用。如果您不这样做,ASP.NET 会在文档末尾附加一个<div>元素,这会导致您的 XML 出现两个根节点,从而使其无效。
  • 您必须清空Response对象,以防其他代码已经缓冲了一些内容要发送回浏览器。您只想在输出中获得网站地图的 XML,仅此而已。
  • 您需要将 HTTP 状态码设置为 200,以确保客户端看到其请求的结果是成功的。Google 爬虫只喜欢成功。
  • 您必须将Response的 MIME 类型设置为 text/XML,因为这是搜索引擎爬虫期望的文档类型。
  • 最后,获取 XML 文档的OuterXml属性,并将其Write()回浏览器,然后再结束 Response。

配置 IIS 将请求转移到错误处理程序页面

要使上面定义的页面能够处理 HTTP 404 错误,首先必须在 IIS 中注册它来处理这些错误。请记住,您可以将同一个 ASPX 页面注册为处理 ASP.NET 类型页面和非 ASP.NET 类型页面的错误。但是,对于由 ASP.NET 处理的文件类型,您使用web.config来注册它们。由于 XML 类型不由 ASP.NET 引擎处理,您需要告诉 IIS 这个新页面,这无法通过web.config文件完成。相反,您必须使用 IIS 管理控制台来注册错误处理程序页面。Microsoft TechNet 网站上提供了关于此主题的非常好的说明此处 [^]。在我使用 IIS 管理控制台的测试站点上,ErrorNotFound404.aspx页面的注册如下所示。

Screenshot - ConfiguringIISErrors.gif

结论

如上所述,您也可以通过web.config文件将同一个页面注册为 ASP.NET 的错误处理程序。只是请注意,当 ASP.NET 引擎处理错误时,它会将浏览器重定向到您指定的页面。所以,如果您依赖于到错误处理程序的干净传输,您可能无法获得您想要的结果。但是,对于sitemap.xml文件,上面显示的方法非常干净,因为 IIS 处理丢失文件的方式。完成后,使用Fiddler2 Web Debugging Proxy [^] 打开您的sitemap.xml文件,并使用会话检查器来准确查看线路上发生了什么。您将看到这段代码如何让搜索引擎爬虫看到这个缺失文件的潜在 404 错误变得多么干净。

最后的想法

如果您可以使用此技术动态生成sitemap.xml文件,您可能还可以用它来生成几乎任何类型的虚拟文件:robots.txt、RSS feed 等。这意味着您网站的更多内容可以从数据库内容动态生成。好好想想。享受吧!

本文历史

  • 2007 年 11 月 24 日 - 初版发布
  • 2007 年 11 月 28 日 - 文章编辑并移至 CodeProject.com 主要文章库
© . All rights reserved.