ASP.NET и немного поисковой оптимизации

С тех пор, как появилась Альтависта, большинство посетителей стали приходить на сайты из поисковых машин. И головной боли стало больше. Теперь мало написать сайт, нужно сделать его совместимым с поисковыми ботами. Одна из тех задач, которую приходится решать — быстрая переиндексация обновлений на сайте. Поисковые роботы чаще посещают те сайты, которые чаще меняются.

Для этого они посылают запрос с заголовком If-Modified-Since, а также обрабатывают заголовок Last-Modified, который возвращает сервер. Именно так роботы получают информацию о времени последнего изменения страниц, и могут «оценить» частоту обновлений.

И Apache, и IIS корректно обрабатывают такие запросы, если речь идёт о статических страницах. Но у них возникают проблемы с динамическими страницами (это касается и PHP, и ASP.NET) — они не могут использовать время последнего изменения файла. Что, если код написан месяц назад, а новость из базы данных опубликована сегодня утром?

С другой стороны, мы могли внести правки в мастер-страницу (master page) или пользовательский элемент управления (ascx-файл). Эти модификации влияют на содержимое страницы, которое «видит» поисковый робот, и, значит, время её последнего изменения должно быть новым.

Вероятно, вы думаете, что нам придётся искать все файлы, связанные с aspx-страницей и выбирать среди них самый новый? Нет. Один простой трюк позволит уместить весь наш код в одну строку.

Решение

Дата и время последнего изменения страницы

ASP.NET перекомпилирует страницу каждый раз, когда меняется она, или любая из тех страниц, от которых она зависит. Результат компиляции, dll-файл, размещается во временной папке ASP.NET, например, в C:WINDOWSMicrosoft.NETFrameworkv2.0.50727Temporary ASP.NET Files.

Страница в ASP.NET — это наследник класса System.Web.UI.Page. С помощью механизма рефлексии, мы можем получить ссылку на сборку, у которой в свойстве Location и хранится имя dll-файла. Я не преувеличивал, когда писал о решении в одну строку:

DateTime lastModified = File.GetLastWriteTime(Page.GetType().Assembly.Location);

Здесь учитываются изменения кода или шаблона страницы, а так же её мастер-страниц и пользовательских элементов управления.

Компонент

Давайте позаботимся об удобстве решения. Проще всего представить его в виде элемента управления:

<binateq:LastModifiedHandler ID=”lastModifiedHandlerrunat="server" />

Этот элемент управления не генерирует HTML-код, но обрабатывает If-Modified-Since и возвращает Last-Modified.

Кроме того, он предоставляет метод Assign для того, чтобы можно было установить время последнего изменения из внешних источников, например, из базы данных. Предположим, что у нас есть таблица News, где поле created хранит время создания новости. Тогда с помощью простого кода мы можем передать его компоненту lastModifiedHandler:

using(SqlCommand cmd = connection.CreateCommand())
{
 cmd.CommandText = “SELECT MAX(created) FROM News”;
 lastModifiedHandler.Assign((DateTime)cmd.ExecuteScalar());
}

Естественно, компонент сохраняет только самое позднее время, поэтому его можно применять с разными источниками внутри одной и той же страницы. Кстати, если бы код был не учебным, а рабочим, я бы лично проверял возвращаемое значение на null. Предупреждаю на случай, если решите использовать метод copy-paste.

Реализация

Для начала решим проблему хранения даты/времени последней модификации:

private DateTime _lastModified = DateTime.MinValue;

public void Assign(DateTime lastModified)
{
        if(_lastModified < lastModified)
                _lastModified = lastModified;
}

Последовательный вызов метода Assign сохранит самые поздние дату/время, из тех, что передавались ему в качестве параметра. (Кстати, подскажите более удачное название для метода. Set мне кажется слишком прямолинейным, как будто значение действительно изменяется, но это не всегда так. Update и Assign тоже прямолинейны, но ничего более удачного в голову мне пока не пришло.)

Всю работу по обработке и подготовке заголовков мы будем проводить в переопределённом методе OnPreRender. Почему именно здесь?

Потому что отклик на запрос If-Modified-Since предполагает, что сервер либо вернёт свой обычный ответ со статусом 200 Ok (и это значит, что страница изменилась), либо 304 Not Modified. В последнем случае нет необходимости генерировать HTML-код, так что можно снизить нагрузку на сервер.

С другой стороны, значения из базы данных появляются на этапе обработки событий, таких как Button_Click или Repeater_ItemDataBound, значит, наш код должен выполняться после них.

Метод OnPreRender вызывается как раз в нужное время, и идеально нам подходит:

protected override void OnPreRender(EventArgs e)
{
        base.OnPreRender(e);
        Assign(File.GetLastWriteTime(Page.GetType().Assembly.Location));
        DateTime utcLastModified = _lastModified.ToUniversalTime();

        DateTime utcModifiedSince;

        if(DateTime.TryParseExact(Page.Request.Headers["If-Modified-Since"], "R", CultureInfo.InvariantCulture, DateTimeStyles.None, out utcModifiedSince))
        {
                if(utcModifiedSince > utcLastModified)
                {
                        Page.Response.AppendHeader("Content-Length", "0");
                        Page.Response.StatusCode = 304;
                        Page.Response.StatusDescription = "Not Modified";
                        Page.Response.End();
                }
        }

        Page.Response.AppendHeader("Last-Modified", utcLastModified.ToString("R"));
}

Код получился тривиальным (надеюсь, это порадует любителей простых статей, которые оставили комментарии в прошлый раз). Метод ToUniversalTime переводит локальное время в гринвичское, поскольку именно его предпочитает протокол HTTP.

Дата и время в HTTP передаются в соответствии со стандартом RFC1123. Ему в .NET соответствует формат “R”, который используется при вызове DateTime.TryParseExact и DateTime.ToString.

Если мы получили заголовок If-Modified-Since (Request.Headers) и сумели его разобрать (TryParseExact), сверяем дату и время. Обнаружив, что страница не изменялась, устанавливаем статус 304 Not Modified и прекращаем обработку запроса.

Без If-Modified-Since мы возвращаем заголовок Last-Modified с датой и временем последнего изменения нашей страницы.

Заключение

Кажется, это всё. Чтобы превратить код в компонент поместите его в класс:

[AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
[AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
[ParseChildren(false)]
[ToolboxData("<{0}:LastModifiedHandler runat="server" />")]
public class LastModifiedHandler: Control
{

}

Остался всего один вопрос. А можно ли упростить назначение даты и времени из внешних источников? Неужели всё время придётся писать небольшой, но надоедливый код?

Сейчас я пытаюсь решить эту проблему с помощью рефлексии. Если вам интересны результаты исследований, пишите, оформлю их в виде статьи.


Автор: http://markshevchenko.habrahabr.ru/



Опубликовал admin
30 Июл, Среда 2008г.



Программирование для чайников.