Jak na SEO-friendly URL (Cool URL) v ASP.NET (obdoba mod-rewrite pro PHP)
- Vložil Trupík 2/22/2006 11:31:36 PM
-
Co jsou SEO-friendly URL (nebo také Cool URL)?
(pokud víte, tuto část přeskočte)
Typický scénář dynamické webové stránky je toto: vytvoří se nějaká kostra (layout)
stránek (například rozložení navigace a grafiky, loga), které se bude na všech
stránkách opakovat. V ní se také vyznačí místa pro dynamicky vkládaný obsah.
Ten je pak typicky načítán z databáze (u článků, textů) nebo z dalších souborů
(třeba obrázků).
To, jaký obsah má pro tu kterou stránku načíst, se obvykle server dozvídá z
parametrů URL - to je ta část URL, která je za otazníkem,
jednotlivé parametry se pak oddělují ampersandem (&).
URL může mít třeba takovýto tvar na serveru používajícím PHP:
http://www.casopis.cz/clanek.php?ID=1234
nebo na IIS serveru používajícím ASP.NET
http://www.casopis.cz/clanek.aspx?ID=1234
Server se z parametru ID dozví, že má načíst článek číslo 1234.
V databázi se najde článek 1234 (předpokládejme, že má titulek
"Umlátila dítě deštníkem"
) a zobrazí se.
Místo toho ale lze vytvořit pomocí serverových mechanismů imitaci
statických url a článek nabízet třeba na adrese tvaru
http://www.casopis.cz/clanky/1234-umlatila-dite-destnikem.aspx
SEO friendly URL dneska frčí, URL ve tvaru www.domena.cz/clanek.aspx?id=1234
se zkrátka už nenosí. Jako hlavní argument pro "Cool" url se uvádí, že jsou lépe
hodnocené vyhledávači. To možná platilo nedávno, dnes bych řekl, že
url /clanky/nazev-clanku a /clanky?titulek=nazev-clanku už budou hodnoceny
poměrně rovnocenně. Také se říká, že cool url jsou snáze zapamatovatelné a
dají se snáze nadiktovat. To je dost subjektivní, ale je pravda, že v metodě,
kterou budu používat, stačí jako jednoznačná identifikade url /clanek/1234.
Ale asi nikde jsem si nepřečetl argument, který přesvědčil mě.
Já se totiž předtím než kliknu na odkaz podívám, kam odkaz vede
(najedu na něj kurzorem). A z hezké URL se toho dozvím tolik, že už
vím, co můžu od odkazované stránky očekávat (co se za odkazem skrývá).
Pomocí ASP.NET lze celkem snadno dosáhnout kýženého efektu.
Ukážu jak přesměrovat z cool url na interní url a jak vracet kód 301 při
přímém požadavku na staré url (aby se zamezilo duplicitám).
Nejdřív o co se budu snažit.
- předpokládám, že stránka vnitřně používá soubor clanek.aspx
k zobrazení článků, url parametr id slouží jako primární klíč do
tabulky článků, www.domena.cz/clanek.aspx?id=1234
vrátí článek s id 1234
- chci přejít na „hezký“ tvar domény www.domena.cz/clanky/1234-titulek-clanku.aspx
přičemž text za číslem (id)článku nás nezajímá a může být
libovolný
- chci, aby i nadále byly funkční odkazy „starého typu“,
ale místo přímého zobrazení se provede přesměrování na odkaz nového
typu tím se zamezí tomu, že vyhledávače budou stránku na nové a staré url
považovat za dvě různé stránky se stejným obsahem. Toto provedeme
zasláním HTTP hlavičky 301 – Moved Permanently
- na nových url samozřejmě musí fungovat korektní postback články
s id které v databázi nemáme budou vrace hlavičku 404
– File Not Found (resp. se provede přesměrování na vlastní
stránku obsluhující chybu 404).
Metoda RewritePath
Před zpracováním požadavku se vždy volá serverová metoda Application_BeginRequest,
její tělo napíšeme do souboru global.asax umístěného v rootu aplikace. Prázdné tělo vypadá takto:
void Application_BeginRequest(Object sender, EventArgs e)
{
}
V těle metody lze přistupovat na objekt HttpContext a volat klíčovou metodu
RewritePath. Ta provede serverové přemapování URL a po odchodu z těla
Application_BeginRequest se pokračuje v zpracování požadavku na nové URL
(ale stále je to ten samý požadavek). Klient nemá šanci poznat, že k
přemapování dochází, vše se děje na serveru. Takto může vypadat tělo
funkce pro vzorový příklad
void Application_BeginRequest(Object sender, EventArgs e)
{
string url = HttpContext.Current.Request.Url.PathAndQuery;
string s_id;
Match match;
Regex reg = new Regex("/clanky/(\\d+).*");
match = reg.Match(url, 0);
if (match.Groups[1].Success)
{
s_id = match.Groups[1].Value;
// musime vylezt z adresare clanky "o adresar vys"
// adresar clanky nemusi realne vubec existovat
HttpContext.Current.RewritePath("../clanek.aspx?id=" + s_id);
return;
}
}
Po zavolání RewritePath se skutečně provede přepis URL a správně se začne
zpracovávat stránka clanek.aspx. Vše funguje, ale až do momentu, kdy dojde
na stránce clanek.aspx k postbacku (třeba když byste chtěli umožnit vkládat
komentáře k článkům). Protože stránka clanek.aspx vůbec netuší to, že na ni
byla přemapována jiná URL, chová se dál jakoby k němu nedošlo a postbackuje
na stránku clanek.aspx?id=xxxx, tedy na "ne cool" url. Tomu je tedy potřeba
zabránit, uživatele i roboty by to jistě nepěkně mátlo, navíc by mohlo dojít
i k dalším problémům. Jedním z možných řešení je zavolat funkci RewritePath
ještě jednou a to v metodě Page_Load stránky clanek.aspx. Když jsem psal, že
nová stránka nic neví o tom, že na ni bylo přemapováno jiné URL, není to tak
úplně pravda. Původní (cool) URL je totiž přístupné v atributu Request.RawUrl.
Na něj lze přepsat v Page_Load, ovšem až po přečtení parametru id:
protected void Page_Load(object sender, EventArgs e)
{
string s_id;
int id;
s_id = Request.Params["id"];
Context.RewritePath(Request.RawUrl, "", "", true);
if (Int32.TryParse(s_id, out id))
{
// tady se muze zpracovat id clanku
}
}
Použil jsem přetížení metody RewritePath. Druhý parametr (pathInfo) nevím k čemu
slouží, třetí parametr nastavují záměrně na prázdný řetězec - třetím parametrem
je totiž QueryString a ten chci, aby byl prázdný (z URL tak vypadne parametr id).
Posledním parametrem je boolovské setClientFilePath. Nastavení na true umožní
správné přemapování postbacků a adres serverových prvků.
První část bychom měli již mít úspěšně za sebou, URL se jedním směrem úspěšně
přepisují. Nyní se podíváme na opačný přepis - staré URL teď nebudeme vracet,
ale místo toho vrátíme HTTP hlavičku 301 (trvale přesunuto) a nové umístění
(novou URL). O tomto se dozví prohlížeč a také to sám pořeší - typicky tak,
že po obdržení kódu 301 okamžitě načte nové umístění dokumentu a novou URL
napíše do adresového řádku. Requesty na staré URL odchytíme také v těle
Application_BeginRequest.
Regex reg2 = new Regex("/clanek.aspx\\?id=(\\d+).*");
match = reg2.Match(url);
if (match.Groups[1].Success)
{
s_id = match.Groups[1].Value;
HttpContext.Current.Response.StatusCode = 301;
HttpContext.Current.Response.Status = "301 Moved Permanently";
HttpContext.Current.Response.StatusDescription = "Moved Permanently";
HttpContext.Current.Response.RedirectLocation = "clanky/" + s_id;
return;
}
Obsluha všech koncovek souborů
Cool URL v tomto stavu sice fungují, ale jen za podmínky, že se před zpracováním
požadavku nejprve zavolá metoda BeginRequest. A ta se zavolá jen pro ty požadavky,
které zpracovává ASP.NET - tedy ty s koncovkou aspx apod. Pokud byste chtěli
aplikovat přepisování i třeba na koncovku html nebo i na soubory bez koncovky
(asi nejvíc cool řešení www.domena.cz/clanky/1234-titulek), je potřeba zajistit,
aby ASP.NET zpracovávalo i tyto koncovky. To bohužel (ale je to logické) nelze
nastavit ve vaší aplikaci, ale musí se odpovídajícím způsobem nakonfigurovat
IIS server (takže pokud jste na hostingu, musíte o toto nastavení
požádat administrátora). Toto nastavení provedete následujícím způsobem:
- Otevřete konzoli IIS
- Vyberte web (např. Default Web Site ve Windows XP), klikněte pravým
tlačítkem a zvolte Properties
- Zvolte záložku Home Directory a klikněte na Configuration
- Klikněte na Add
- Jako Executable zadejte cestu k aspnet_isapi_dll (typicky
c:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll)
a Extension zadejte .*, odškrtněte checkbox „Check that file
exists“ a potvrďte všechny otevřené dialogy
Pozor na relativní adresy
Při přepisování URL mějte na paměti, že všechny relativní adresy statických prvků (atributy href u odkazů, src u obrázku apod) se předávají v relativní podobě až k prohlížeči a až ten je doplní podle aktuální URL. A protože prohlížeč vidí již přepsanou cool URL, je potřeba, aby relativní adresy byly psány vzhledem k cool URL. Nebo používejte absolutní adresy.
(vlastní) Stránka 404
Také budem zachytávat přístupy na neplatná id. Předpokládejme, že všechna id
větší než 100 jsou neplatná a povedou na chybovou stránku 404
(soubor nenalezen). Upravíme trochu první část (rozpoznání cool url)
Regex reg = new Regex("/clanky/(\\d+).*");
match = reg.Match(url, 0);
if (match.Groups[1].Success)
{
s_id = match.Groups[1].Value;
if (!Int32.TryParse(s_id, out id) || id > 100)
{
Response.StatusCode = 404;
Response.End();
return;
}
HttpContext.Current.RewritePath("../clanek.aspx?id=" + s_id);
return;
}
Standardně kód 404 obsluhuje prohlížeč - zobrazí informační stránku s textem,
že požadovaná stránka není k dispozici nebo něco podobného. Toto chování
můžete změnit tím, že si vytvoříte vlastní chybovou stránku. Může obsahovat
třeba odkaz na homepage, vyhledávání nebo návrhy uživateli, kam pokračovat,
aby našel to, co hledal na neexistující URL. Až takovou stránku vytvoříte
(může to být standardní html nebo aspx stránka), je potřeba upravit nastavení
ve web.config, aby se stránka předávala jako 404. Vložte tento kód do souboru
web.config dovnitř uzlu configuration/system.web
<customErrors mode="On">
<error statusCode="404" redirect="/netstudent/fnf.html"></error>
</customErrors>
Problém je, že ASP.NET přistupuje k chybovým stránkám na můj vkus dost podivně. Pokud nastavíte web.config výše uvedeným způsobem a zadáte neexistující URL, ASP.NET zjistí že stránka neexistuje a podle web.configu provede přesměrování na chybovou stránku fnf.html. Takže dotaz na neexistující stránku vrací nejprve HTTP 302 (Found) - tedy dočasně přesunuto. Klientovi tedy oznamuje, že požadovaný obsah se dočasně nechází na adrese fnf.html. Po přesměrování vrací chybová stránka fnf.html HTTP 200 (OK). A to vůbec nepopisuje reálnou situaci! Server by měl odpovědět na neexistující URL kódem 404 a ne oznámit, že požadovaný dokument je nyní (dočasně!) na adrese fnf (tedy na chybové stránce). Tady to někdo překombinoval...
Pokud nastavíte programově chybový kód na 404 (Response.StatusCode = 404), tak se skutečně odešlo HTTP 404, ale už se zase nezobrazí vlastní chybová stránka fnf.html, ale místo toho se nechá zobrazení chybové stránky na prohlížeči, takže vaše pracně budovaná stránka fnf.html je v tomto případě k ničemu (ale pořád lepší než vracet 302 na dotaz na neexistující URL).
Jen pro doplnění, pokud použijete StatusCode = 301 nebo 302, tak vše funguje tak, jak očekáváte - klient (prohlížeč) dostane správný HTTP kód a sám provede přesměrování na novou adresu, kterou nastavíte Response.RedirectLocation = "...".
Na to, jak by podle mne měla vypadat chybová stránka 404 se můžete podívat třeba na Wikipedii:
http://cs.wikipedia.org/neexistujiciurl
Dotaz vrátí kód 404 a vlastní chybovou stránku Wikipedie, která vás srozumitelně informuje o problému a navrhuje vám řešení (zobrazit výsledky vyhledávání slova "neexistujiciurl" ve Wikipedii.
Docílit podobného chování v ASP.NET se mi nepodařilo - buďto se povede zaslat 404, ale neprovede se přesměrování, nebo se sice úspěšně přesměruje, ale zase server vrací 302.
Alternativní způsob přepisování adres - IHttpHandlerFactory
Narazil jsem také na další způsob přepisování adres, který nijak neupravuje kód v global.asax. Místo toho se vytvoří trída implementující rozhraní IHttpHandlerFactory, která bude sama zpracovávat jí předané požadavky. Potom jde o to odfiltrovat správné požadavky - ty se nastaví ve web.configu. Parametry se předávají přes kolekci context.Items, která je přístupná během celého procesu zpracování jednoho požadavku. Třída může vypadat třeba takto:
public class Remapper : IHttpHandlerFactory
{
public Remapper()
{
}
#region IHttpHandlerFactory Members
public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
{
context.Items["id"] = System.IO.Path.GetFileNameWithoutExtension(pathTranslated);
return PageParser.GetCompiledPageInstance("~/clanek.aspx",
context.Server.MapPath("~/clanek.aspx"),
context);
}
public void ReleaseHandler(IHttpHandler handler)
{
}
#endregion
}
Uložený parametr id si můžeme přečíst v Page_Load stranky clanek.aspx z Context.Items["id"]. Web config je třeba nastavit takto: do uzlu configuration/system web přidejte
<httpHandlers>
<add verb="*" path="clanky/*" type="Remapper"/>
</httpHandlers>
Více o této metodě si můžete přečíst zde (mimochodem, i tato stránka používá cool url)
http://www.aspnet.cz/Articles/44-tovarna-na-absolutni-url-rewriting-pomoci-ihttphanderfactory.aspx
PHPkáři pro Cool URL obvykle používají mod-rewrite - doplněk webového serveru
Apache, na kterém běží většina webů používající PHP. Ten funguje tak, že v konfiguračním
souboru nastavíte pravidla přepisování URL a o zbytek se postará server.
O implementaci SEO-friendly URL pro Apache a PHP pomocí mod_rewrite se můžete dočíst v článku:
Jak na SEO-friendly URL (a další věci) pomocí mod_rewrite pro Apache a PHP
Ohodnoťte prosím užitečnost článku