Замена URL с помощью ASP.NET для поисковой оптимизации

ОГЛАВЛЕНИЕ

Замена URL – процесс скрытия сложного URL с параметризованной строкой запроса, путем использования серий единообразных, часто словесных URL, не содержащих параметров строки запроса.

•    Скачать демо обработчика ".html" (требует IIS) - 6.03 Кб
•    Скачать демо обработчика ".ashx" (не требует IIS) - 6.12 Кб

Введение

Идея в том, что единообразный URL запрашивается с сервера, и внутри определяются требуемые параметры, затем вызывается оригинальный URL со строкой запроса.

Чаще всего это делается ради SEO (Search Engine Optimisation - поисковая оптимизация). Поисковики обычно не любят параметры строки запроса у веб-страниц, так как они означают динамически меняющуюся страницу, которую труднее индексировать, потому что одна и та же страница часто встречается, содержит запутанные бесполезные данные и не считается удобной для клиента. В статье рассказано именно о замене на единообразные ресурсы страниц HTML в скриптах ASPX с запросами.

Первая проблема заключается в том, что по умолчанию IIS сам обрабатывает все запросы к ресурсам *.htm и *.html и отвечает ошибкой “404 Страница не найдена”, если такой запрос не существует. Чтобы заменить страницы *.html, надо приказать IIS перенаправлять ASP.NET все запросы к этим ресурсам и не обрабатывать их самому. Можно маршрутизировать *.html через ASP.NET и не трогать *.htm, а можно маршрутизировать любые ресурсы, перенаправляя ASP.NET все запросы. Это можно сделать в IIS6, перейдя в свойства веб-сайта, нажав кнопку “Конфигурация” на вкладке “Домашний каталог” (или на вкладке “Виртуальный каталог” для виртуальной папки приложения) и добавив привязку для расширений, которые надо перенаправлять расширению ASP.NET ISAPI. Он обычно расположен в C:\Windows\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll, но всегда можно найти и скопировать свойства для привязки *.aspx в случае сомнения. Также обязательно снимите выделение с опции проверки существования запрошенного ресурса.

Как только запрос дойдет до ASP.NET, выполняется замена URL с помощью метода HttpContext.RewritePath. Этот метод меняет информацию исходного запроса, переданного из IIS, на другой URL. Замену можно производить в различных местах, в том числе в обработчике приложения global.asax, в модуле HTTP или в обработчике HTTP.

В отличие от других статей, больше описывающих процесс преобразования удобных URL в серверные URL с помощью файла конфигурации и регулярных выражений, эта статья излагает основы, требуемые для построения надежной системы замены URL в ASP.NET с помощью различных подходов. Был реализован простой метод-заглушка для модуля и обработчика HTTP, расширяемые в соответствии с диктуемыми требованиями.


Замена с помощью global.asax

Файл global.asax позволяет обрабатывать события уровня приложения и сессии и находится в корневой папке приложения. Можно реализовать простую замену URL с использованием обработчика события Application_BeginRequest из этого файла, вызываемого всякий раз, когда новый запрос отправляется ASP.NET из IIS на обработку:

void Application_BeginRequest(object sender, EventArgs e)
{
    HttpApplication app = sender as HttpApplication;
    if(app.Request.Path.IndexOf("FriendlyPage.html") > 0)
    {
        app.Context.RewritePath("/UnfriendlyPage.aspx?SomeQuery=12345");
    }
}

В фрагменте кода выше заменяются любые запросы к странице FriendlyPage.html на страницу UnfriendlyPage.aspx со строкой запроса SomeQuery=12345. По мере прохождения запроса по конвейеру он будет использовать вновь замененный ресурс вместо исходного.

Это очень простой, жестко заданный пример замены URL, не учитывающий пути приложения. Обычно замена не выполняется в виде жестко заданных записей в обработчике global.asax, а осуществляется в специальном модуле HTTP или с помощью обработчика HTTP.

Как сказано ранее, пример выше будет работать, только если IIS настроен на отправку ASP.NET запросов к ресурсу *.html, но этот пример с тем же успехом работает для любого типа запроса, включая каталог (если в IIS задана привязка *) и запросы к другим страницам aspx.

Замена с помощью модуля HTTP

Модуль HTTP является классом, реализующим интерфейс IHttpModule. Он требует реализации двух методов:
•    Init, применяемый для подключения событий конвейера, которые модуль должен обрабатывать
•    Dispose – освобождает любые выделенные ресурсы

Замена URL посредством модуля HTTP работает аналогично показанному ранее подходу global.asax. Модули HTTP встраиваются в конвейер обработки приложения ASP.NET путем определения их в файле web.config. ASP.NET автоматически загрузит и создаст экземпляры всех определенных модулей и вызовет их методы Init(). Метод Init() можно использовать для подписки на другие события в конвейере запроса.

Модули HTTP выполняются последовательно, один за другим, в порядке их указания в файле web.config, и их методы вызываются до эквивалентных событий в global.asax.
Следующий фрагмент показывает, как пишется типичный (очень простой) модуль HTTP:

public class UrlRewritingModule : IHttpModule
{
    public UrlRewritingModule()
    {
    }

    public String ModuleName
    {
        get
        {
            return "UrlRewritingModule";
        }
    }

    const string ORIGINAL_PATH = "OriginalRequestPathInfo";

    public void Init(HttpApplication application)
    {
        application.AuthorizeRequest += new EventHandler(application_AuthorizeRequest);
        application.PreRequestHandlerExecute +=
        new EventHandler(application_PreRequestHandlerExecute);
        application.Context.Items[ORIGINAL_PATH] = null;
    }

    void application_PreRequestHandlerExecute(object sender, EventArgs e)
    {
        HttpApplication app = sender as HttpApplication;
        String strOriginalPath = app.Context.Items[ORIGINAL_PATH] as String;
        if (strOriginalPath != null && strOriginalPath.Length > 0)
        {
            app.Context.RewritePath(strOriginalPath);
        }
    }

    void application_AuthorizeRequest(object sender, EventArgs e)
    {
        HttpApplication app = sender as HttpApplication;
        String strVirtualPath = "";
        String strQueryString = "";
        MapFriendlyUrl(app.Context, out strVirtualPath, out strQueryString);

        if (strVirtualPath.Length>0)
        {
            app.Context.Items[ORIGINAL_PATH] = app.Request.Path;
            app.Context.RewritePath(strVirtualPath, String.Empty, strQueryString);
        }
    }

    void MapFriendlyUrl(HttpContext context,
    out String strVirtualPath, out String strQueryString)
    {
        strVirtualPath = ""; strQueryString = "";

        // Сделать: Эта процедура должна проверять свойства context.Request и реализовывать
        //       надлежащую систему привязки.
        //
        //       Присвоить strVirtualPath виртуальный путь целевой страницы aspx.
        //       Присвоить strQueryString все строки запроса, необходимые странице.

        if (context.Request.Path.IndexOf("FriendlyPage.html") >= 0)
        {
            strVirtualPath = "~/Main.aspx";
            strQueryString = "Message=You smell of updated cheese!";
        }
    }

    public void Dispose()
    {
    }
}

Как видно, метод Application_AuthorizeRequest делает то же самое, что и процедура в обработчике global.asax Application_BeginRequest. Можно подписаться на событие BeginRequest в модуле HTTP, но вместо этого лучше использовать AuthorizeRequest, так как он учитывает аутентификацию форм, которая может выполнять перенаправление для получения регистрационных данных; если это происходит и URL уже был заменен событием BeginRequest, то клиент будет отправлен обратно на замененную страницу, а не на исходную удобную страницу. Осуществление замены внутри события AuthorizeRequest гарантирует, что подсистема аутентификации форм вернет клиента на удобное имя ресурса.

Для осуществления привязки был реализован метод-заглушка MapFriendlyUrl, выясняющий, как надо заменить запрошенный ресурс. Это полностью зависит от ваших настроек. В данном примере используется небрежно сделанный жестко заданый тест для любого запроса к “FriendlyPage.html”, преобразуемый в “UnfriendlyPage.aspx?FirstQuery=1&SecondQuery=2”. Разумеется, надо дополнить этот метод, чтобы он делал то, что вам надо, следя, чтобы выходные параметры “strVirtualPath”  и “strQueryString” были заполнены. Если ресурс не удалось преобразовать, метод возвращает пустой путь.

В обработчике PreRequestHandlerExecute есть дополнительный вызов метода RewritePath. Это сделано, потому что любая форма отправляется обратно, что происходит при отправке целевой страницы обратно на исходный удобный URL, а не на неприятный замененный. В примере выше все запросы к "FriendlyPage.html” заменяются на “UnfriendlyPage.aspx?FirstQuery=1&SecondQuery=2”. Если пропустить вторую замену на этапе PreRequestHandlerExecute, все обратные отправки на странице UnfriendlyPage.aspx отправятся обратно на UnfriendlyPage.aspx, а не на FriendlyPage.html.

Однако есть один интересный побочный эффект; несмотря на то, что сама страница восстанавливается, любые запросы, используемые в замененной странице, все еще появятся как часть ссылки обратной отправки. Есть некоторые интересные идеи по решению данной проблемы, в том числе переопределение интерпретации атрибута action тега <form> страниц с помощью адаптера контроля CSS, но можно более ловко избежать этой проблемы, используя обработчик HTTP вместо модуля HTTP, что позволяет использовать тот же прием двойного RewritePath, но в лучшем месте в конвейере.

Другая любопытная особенность заключается в том, что используется свойство управления состоянием HttpContext.Items для хранения и разделения пар ключ/значение между отдельными каркасными вызовами обработчиков событий модуля HTTP. Это свойство также помогает передавать информацию о состоянии между модулями HTTP и обработчиками HTTP. В таком случае необходимо записать исходный путь, чтобы восстановить его позже в конвейере, и снова заменить на него.

Замена с помощью обработчика HTTP

Обработчик HTTP является классом, реализующим интерфейс IHttpHandler, служащим для реализации специальной реакции на конкретный тип запроса к ресурсу, оправленному механизму ASP.NET. Файл web.config приложения содержит привязки, указывающие, какой обработчик какими ресурсами должен заниматься и для каких команд HTTP. Например, ASP.NET умеет использовать стандартный обработчик страницы всегда, когда поступает запрос к ресурсу *.aspx. Напротив, модули HTTP вызываются для всех запросов, и приходится определять, надо ли что-то делать с запросом внутри модуля.

Чтобы произвести замену для ресурсов *.html, надо создать обработчик, занимающийся запросами *.html. Интерфейс IHttpHandler требует реализации двух методов:
•    ProcessRequest, вызываемый каркасом ASP.NET, когда соответствующий запрос требует обработки, и обязанный реализовывать надлежащий ответ.
•    IsReusable, возвращающий логический флаг, показывающий, может ли использоваться один и тот же обработчик для множественных запросов или нет.

Типичная ошибка, допускаемая при реализации замены с помощью обработчика HTTP, заключается в том, что вызывается метод HttpContext.RewritePath для замены запроса, и ожидается, что он как-то будет работать. Обработчик HTTP должен по-настоящему обрабатывать запрос, чтобы дать ответ. Простая замена запроса *.html на запрос *.aspx работает в модуле HTTP или в обработчике global.asax, потому что каркас ASP.NET вызывает правильный обработчик (т.е. стандартный обработчик страницы *.aspx), потому что информация запроса была заменена до выбора и вызова нужного обработчика. Как только обработчик был вызван, он отвечает за генерацию правильного ответа.

Чтобы реализовать обработчик HTTP для обработки ресурсов *.html, надо воспользоваться стандартным обработчиком страницы и перенаправить ему замененный запрос после замены исходного URL. К счастью, можно использовать метод PageParse.GetCompiledPageInstance для возврата экземпляра стандартного обработчика страницы для конкретного ресурса. Задав экземпляр стандартного обработчика страницы для целевого ресурса aspx, можно непосредственно вызвать его метод ProcessRequest из метода ProcessRequest собственного обработчика.
Поскольку используется метод GetCompiledPageInstance для возврата экземпляра страницы, исходя из требуемой целевой страницы aspx, не надо использовать метод RewritePath для изменения запрошенной страницы, только для строк запроса. Ниже приведена реализация простого обработчика HTTP:

public class UrlRewriter : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        // Привязать удобный URL к серверному..
        String strVirtualPath = "";
        String strQueryString = "";
        MapFriendlyUrl(context, out strVirtualPath, out strQueryString);

        if(strVirtualPath.Length>0)
        {
            // Применить требуемые строки запроса к запросу
            context.RewritePath(context.Request.Path, string.Empty, strQueryString);

            // Получить обработчик страницы для требуемой страницы ASPX с использованием этого контекста.
            Page aspxHandler = (Page)PageParser.GetCompiledPageInstance
        (strVirtualPath, context.Server.MapPath(strVirtualPath), context);

            // Выполнить обработчик..
            aspxHandler.PreRenderComplete +=
        new EventHandler(AspxPage_PreRenderComplete);
            aspxHandler.ProcessRequest(context);
        }
    }

    void MapFriendlyUrl(HttpContext context,
    out String strVirtualPath, out String strQueryString)
    {
        strVirtualPath = ""; strQueryString = "";

        // Сделать: Эта процедура должна проверять
        // свойства context.Request и реализовать
        //       надлежащую систему привязки.
        //
        //       Присвоить strVirtualPath виртуальный путь целевой страницы aspx.
        //       Присвоить strQueryString все строки запроса, необходимые странице.

        if (context.Request.Path.IndexOf("FriendlyPage.html") >= 0)
        {
            // Пример жестко заданной привязки "FriendlyPage.html"
       // к "UnfriendlyPage.aspx"

            strVirtualPath = "~/UnfriendlyPage.aspx";
            strQueryString = "FirstQuery=1&SecondQuery=2";
        }
    }

    void AspxPage_PreRenderComplete(object sender, EventArgs e)
    {
        HttpContext.Current.RewritePath(HttpContext.Current.Request.Path,
        String.Empty, String.Empty);
    }

    public bool IsReusable
    {
        get
        {
            return true;
        }
    }
}

Сначала обработчик определяет целевую страницу aspx, требуемую для обработки запроса, и определяет все строки запроса, которые должны быть переданы целевой странице. Для простоты не была включена никакая конкретная реализация привязки – она остается на ваше усмотрение и может быть простыми жестко заданными страницами или включать в себя поиск в конфигурационном файле, содержащем регулярные выражения. Метод MapFriendlyUrl присваивает выходному параметру “strVirtualPath” виртуальный путь целевой страницы aspx, и выходному параметру “strQueryString” присваивает строки запроса, необходимые целевому скрипту.

Далее метод HttpContext.RewritePath применяется для замены только строк запроса (так как путь к исходному удобному URL уже правильный). Затем создается экземпляр стандартного обработчика страницы для целевой страницы aspx. Это делается, потому что при его выполнении страница aspx увидит все требуемые параметры запроса, но все еще будет воспринимать URL как удобный – не придется менять путь запроса к странице aspx, так как она вызывается вручную.

Перед фактической обработкой запроса подключается обработчик события страницы PreRenderComplete. Это событие возбуждается, когда страница завершила создание всех своих управляющих элементов, разбиение на страницы завершено, состояние просмотра готово к записи, и итоговый HTML готов к генерации. Подключение к этому событию позволяет выполнить прием двойного RewritePath (похож на то, что было сделано в подходе модуля HTTP, показанном ранее). Обработчик PreRenderComplete вызывает HttpContext.RewritePath для удаления запросов, добавленных при последней замене (отменяя ее). Это гарантирует, что цель, сгенерированная для обратных отправок, является полностью удобным URL и, в отличие от реализации модуля HTTP, не содержит дополнительных строк запроса, добавленных позже.

Несмотря на то, что теперь обработчик успешно заменяет удобную страницу *.html на требуемые страницы *.aspx с запросами, он все еще имеет несколько проблем:
•    Состояние сессии недоступно внутри целевой страницы aspx
Это проблема для большинства людей, использующих состояние сессии внутри страниц aspx, но ее легко устранить. Добавление интерфейса IReadOnlySessionState или IRequiresSessionState к классу позволяет получить доступ только для чтения или для чтения и записи соответственно. Не надо реализовывать ничего по-другому в обработчике, потому что эти интерфейсы не предоставляют методов и являются маркерными, приказывая ASP.NET активизировать доступ к состоянию сессии при использовании обработчика.
•    Запросы к фактическим файлам *.html больше не обслуживаются.
Это может быть или не быть проблемой. Если все еще необходимо обслуживать фактические статические файлы *.html, то требуется обеспечить, чтобы обработчик делал это. IIS было приказано перенаправлять ASP.NET все запросы к *.html для обработки. Проблема решается путем написания кода в начале (или в конце) обработчика, проверяющего, предназначен ли запрос для реальной страницы, и если да –  обслуживающего его.
•    Удобные URL не могут сами содержать строки запроса.
Пожалуй, это не проблема, так как главная причина для замены – удаление запросов из URL. Однако может пригодиться наличие возможности внутренне вызвать удобную страницу с программными запросами, чтобы поддержать убеждение, что единообразная страница *.html является обслуживаемым ресурсом, вместо того чтобы возвращаться к неприятной странице *.aspx. Необходимо внести изменения, чтобы записать исходные запросы и восстановить их в обработчике события страницы PreRenderComplete. Также необходимо аккуратно объединить привязанные запросы с запрошенными запросами.
•    Обработчик должен изящно обрабатывать ошибки «страница не найдена» при необходимости.
В зависимости от того, как работает система привязки, порой заданная удобная страница фактически ни к чему не относится. Можно перенаправлять ответ на страницу ошибки, или страница aspx может реагировать соответственно, но в противном случае обработчик должен быть написан так, чтобы отвечать надлежащим сообщением и возвращать код ответа 404. Если не сделать этого, то обработчик будет возвращать пустой ответ.


Создание более качественного обработчика HTTP

Ниже приведена полная реализация обработчика HTTP, заменяющего ресурсы *.html на страницы *.aspx со строками запроса, с учетом высказанных ранее соображений.

public class BetterUrlRewriter : IHttpHandler, IRequiresSessionState
{
    const string ORIGINAL_PATHINFO = "UrlRewriterOriginalPathInfo";
    const string ORIGINAL_QUERIES = "UrlRewriterOriginalQueries";

    public void ProcessRequest(HttpContext context)
    {
     //Проверить, существует ли в действительности указанный файл HTML, и если да, обслужить его.
        String strReqPath = context.Server.MapPath
    (context.Request.AppRelativeCurrentExecutionFilePath);
        if (File.Exists(strReqPath))
        {
            context.Response.WriteFile(strReqPath);
            context.Response.End();
            return;
        }

        // Записать PathInfo исходного запроса и
        // информацию QueryString для обработки обратных отправок
        context.Items[ORIGINAL_PATHINFO] = context.Request.PathInfo;
        context.Items[ORIGINAL_QUERIES] = context.Request.QueryString.ToString();

        // Привязать удобный URL к серверному..
        String strVirtualPath = "";
        String strQueryString = "";
        MapFriendlyUrl(context, out strVirtualPath, out strQueryString);

        if(strVirtualPath.Length>0)
        {
            foreach (string strOriginalQuery in context.Request.QueryString.Keys)
            {
                // Чтобы обеспечить сохранение всех строк запроса, переданных в исходном
           // запросе, они добавляются в конец
                // новой строки запроса без добавления ключей,
           // замененных на протяжении обработчика.
                if (strQueryString.ToLower().IndexOf(strOriginalQuery.ToLower()
                                + "=") < 0)
                {
                    strQueryString += string.Format("{0}{1}={2}",
            ((strQueryString.Length > 0) ? "&" : ""),
            strOriginalQuery,
            context.Request.QueryString[strOriginalQuery]);
                }
            }

            // Применить требуемые строки запроса к запросу
            context.RewritePath(context.Request.Path, string.Empty, strQueryString);

    //Получить обработчик страницы для требуемой страницы ASPX с использованием этого контекста.
            Page aspxHandler = (Page)PageParser.GetCompiledPageInstance
        (strVirtualPath, context.Server.MapPath(strVirtualPath), context);

            // Выполнить обработчик.
            aspxHandler.PreRenderComplete +=
        new EventHandler(AspxPage_PreRenderComplete);
            aspxHandler.ProcessRequest(context);
        }
        else
        {
            // Привязка не найдена – генерируется ответ 404.

            context.Response.StatusCode = 404;
            context.Response.ContentType = "text/plain";
            context.Response.Write("Page Not Found");
            context.Response.End();
        }
    }

    void MapFriendlyUrl(HttpContext context, out String strVirtualPath,
                        out String strQueryString)
    {
        strVirtualPath = ""; strQueryString = "";

        // Сделать: Эта процедура должна проверять свойства context.Request и реализовать
        //       надлежащую систему привязки.
        //
        //       Присвоить strVirtualPath виртуальный путь целевой страницы aspx.
        //       Присвоить strQueryString все строки запроса, необходимые странице.

        if (context.Request.Path.IndexOf("FriendlyPage.html") >= 0)
        {
            // Пример жестко заданной привязки "FriendlyPage.html"
       // к "UnfriendlyPage.aspx"

            strVirtualPath = "~/UnfriendlyPage.aspx";
            strQueryString = "FirstQuery=1&SecondQuery=2";
        }
    }

    void AspxPage_PreRenderComplete(object sender, EventArgs e)
    {
        // Надо заменить путь, заменив исходный хвост и строки запроса.
        // Это происходит после загрузки и установки страницы,
        // но гарантирует, что
        // обратные отправки на страницу сохранят URL и запросы исходных незамененных страниц.

        HttpContext.Current.RewritePath(HttpContext.Current.Request.Path,
                        HttpContext.Current.Items[ORIGINAL_PATHINFO].ToString(),
                              HttpContext.Current.Items[ORIGINAL_QUERIES].ToString());
    }

    public bool IsReusable
    {
        get
        {
            return true;
        }
    }
}

Наряду с интерфейсом IHttpHandler задается интерфейс IRequiresSessionState. Это гарантирует получение доступа для чтения и записи к состоянию сессии на протяжении жизненного цикла страницы.

Сначала метод ProcessRequest проверяет, является ли запрошенный ресурс *.html на самом деле запросом к настоящему ресурсу HTML. В зависимости от требований эта проверка может быть ненужной, или она может требоваться после проверки привязок. Но она была включена здесь, так что, несмотря на то, что виртуальные страницы HTML, требующие замены, будут работать с использованием механизма замены, любые запросы к фактическим файлам HTML, действительно существующим в запрошенном месте, получают приоритет и обслуживаются.
Далее используется коллекция управления состоянием ключа/значения HttpContext.Items для сохранения строк запроса и хвоста текущего запроса, чтобы можно было извлечь их обратно в обработчике страницы PreRenderComplete. После вызова метода RewriteUrl для выполнения привязки написан дополнительный код, соединяющий все запросы, заданные в запросе, с запросами, требуемыми для привязки. Это делается так, чтобы заданные запросы привязки получали приоритет и не заменялись таким же параметром запроса из запроса – но при необходимости это можно изменить.

Наконец, если не удается получить привязку, то запрос считается недействительным, и для него выдается ответ 404, и обработка завершается. Можно показывать более качественный ответ, чем реализованный тут, или показывать карту сайта, и т.д.

Удачной замены!

Заключение

Данная статья помогает реализовать замену URL в веб-проектах с помощью одного из показанных решений. Несмотря на то, что эта статья не реализует код для привязки удобных URL к неудобным, она дает реализацию заглушки для расширения и предоставляет основу для развития.

Что если нет доступа к IIS?

Представленная тут информация касается именно замены ресурсов *.html, а значит, требуется возможность добавления привязки в IIS для перенаправления ASP.NET запросов к *.html, однако можно использовать точно такой же процесс для замены любого типа ресурса - при условии, что запрос попадает в модуль ASP.NET ASAPI. Если ваше решение хостинга не позволяет добавлять привязки IIS или же у вас нет доступа к IIS, можно применять показанные тут методы замены с использованием другого типа обрабатываемого ресурса. Например, расширение *.ashx является первым кандидатом на использование вместо *.html, так как запросы *.ashx автоматически привязываются.