Jak na SEO-friendly URL (Cool URL) v ASP.NET (obdoba mod-rewrite pro PHP)

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:

  1. Otevřete konzoli IIS
  2. Vyberte web (např. Default Web Site ve Windows XP), klikněte pravým tlačítkem a zvolte Properties
  3. Zvolte záložku Home Directory a klikněte na Configuration
  4. Klikněte na Add
  5. Jako Executable zadejte cestu k aspnet_isapi_dll (typicky c:\WINDOWS\Mi­crosoft.NET\Fra­mework\v2.0.50727\as­pnet_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




47
 
16
 
13
 
9
 
13
 
 
Vložit komentář:
 

 



 

 

Nepoužívejte žádné html ani texy značky, odřádkování se zachová. Pokud uvádíte zdrojový kód, můžete ho vložit mezi značky
<syntax jazyk="PHP">...</syntax>,
bude potom zformátován. Jako atribut můžete uvést PHP, C#, HTML, CSS a mnoho dalších.


opiste cislo Opište číslo:

 

10/13/2006 3:50:01 AM
[1] (SneakerXZ)
chybky :) odpovědět
Zkoušel sis ten skript spustit? Pokud bys tak udělal zjistil bys, že je tam hromada hrubek :)
gravatar 2/27/2007 11:19:29 PM
[2] (Trupík (jakub.maly(at)atlas.cz)) www
Re: chybky :) odpovědět
[1] Článek jsem od té doby úplně přepracoval. Všechny skripty, které jsou v článku nyní, jsem zkoušel a fungovaly.
3/6/2007 5:27:04 PM
[3] (xsi10)
další pravidla odpovědět
dobrý den, k té první části (pomocí global.asax) mám otázku:
pokud bych chtěl udělat pravidel víc, například: /clanky/ se přepisuje na default.aspx?pg=clanky a pro clanky/jak-neco-udelat/ např default.aspx?pg=clanky&jaky=jak-neco-udelat
tak bych měl použít více výrazů, nebo dát do jediného
Regex reg = new Regex("([a-zA-Z0-9]+)/(a-zA-Z0-9)");


děkuji za odpověď, v ASP.NET jsem začátečník...
gravatar 3/6/2007 9:02:15 PM
[4] (Trupik (jakub.maly(at)atlas.cz)) www
Re: další pravidla odpovědět
[3] Řekl bych, že je to úplně jedno. Jen na konci toho regulárního výrazu by měla být hvězdička. Buďto jeden složitější Regex a složitější parsování výsledku nebo dva Regexy..
3/6/2007 11:46:33 PM
[5] (xsi10)
díky odpovědět
díky moc, funguje, jen mi to přepisuje všechny soubory, jako například styly.css a obrázky :(
gravatar 3/7/2007 9:11:31 AM
[6] (Trupik (jakub.maly(at)atlas.cz)) www
Re: díky odpovědět
[5]: dělá to, co mu řeknete. Pokud řeknete, aby přepisoval všechno, tak taky bude přepisovat.

Já to řeším tak, že požadavky na soubory které skutečně jsou umístěné na daném umístění nepřepisuju. Stačí na to jeden řádek v Application_BeginRequest:
if (System.IO.File.Exists(Server.MapPath(HttpContext.Current.Request.Path))) return;


4/24/2007 9:24:25 AM
[7] (Jakub)
Pěkný článek odpovědět
Děkuji za stručný a vyčerpávající článek.
gravatar 8/4/2007 4:02:48 AM
[8] (Georgeek (vulcain(at)seznam.cz))
podobný problém odpovědět
Řeším podobný problém, také se snažím přemapovávat, ale kvůli obrázkům, které potřebuji mít částečně v databázi, takže stačí GET, jenže to potřebuji hbité a nenáročné na zdroje.

Zkouším to pomocí IHttpHandlerFactory, ale zatím to je nestabilní na Win 2003.

Takže to je takový nápad na možné pokračování.
gravatar 9/29/2007 6:12:12 PM
[9] (Martin V. (m.vodicka(at)gigaart.cz))
problem s rewrite a SqlDataSOurce odpovědět
Dobry den, mam dotaz, na ktery jsem nikde v forech ani u kamaradu nenasel odpoved.

Prepisuji URL pomoci tohoto kodu v Global.asax:
void Application_BeginRequest(Object sender, EventArgs e)
{
string url = HttpContext.Current.Request.Url.PathAndQuery;
string cat_id, subcat_id;
Match match;
Regex reg = new Regex("[^r]/(\\d+)-*");
match = reg.Match(url, 0);
if (match.Groups[1].Success)
{
cat_id = match.Groups[1].Value;
subcat_id = match.Groups[2].Value;
HttpContext.Current.RewritePath("./Default.aspx?cat=" + cat_id);
return;
}
}

v kodu Default.aspx pak muzu snadno k pristoupit k karametru cat takto: Request.Params["cat"], to funguje ale problem je jinde.

-Na strance mam sqldatasource, ze kteryho krmim GridView. SqlDataSOurce je nakonfigurovanej tak, ze filtruje vysledky podle hodnoty QueryStringu (podle parametru cat).

Problem je, ze kdyz pouzivam ten rewrite, tak to s tim SqlDataSource a Gridview prestane fungovat a datovej zdroj nevraci zadny vysledky, jako kdyby vubec zadnej parametr nedostal.

Jeste pro info, pouzivam tam master pages, ale nevim jestli to vadi. Proste ten datovej zdroj vubec ty parametry nedostane, jako by analyzoval tu virtualni adresu misto te prepsane.

Nevite, kde je problem?
gravatar 12/10/2007 9:33:05 AM
[10] (Georgeek (vulcain(at)seznam.cz))
Re: problem s rewrite a SqlDataSOurce odpovědět
[9] http://www.aspnet.cz/Articles/44-tovarna-na-absolutni-url-rewriting-pomoci-ihttphanderfactory.aspx
a
http://www.aspnet.cz/Articles/10-pohled-do-hlubin-webserverovy-duse-aneb-jak-funguji-http-moduly-a-handlery.aspx
gravatar 4/10/2008 7:03:14 PM
[11] (Jan Sikorjak (jan(at)sikorjak.com))
Re: problem s rewrite a SqlDataSOurce odpovědět
[9] Zdravím, také jsem řešil stejný problém a místo náročnějího (na první pohled :) ) řešení přes http handlery jsem si jednoduše přidal na stránku hiddenfield nebo jinou jednoduchou komponentu, jí v Page_Load nastavil value podle QueryStringu a posléze v DataSource nastavil zdroj parametru jako Control a vybral jsem ten hiddenfield. Vím a chápu, že to není nejefektivnější řešení, ale na druhou stranu tím lze jednoduše vyřešit poměrně složitý problém.. Regards..
4/23/2008 12:22:58 PM
[12] (krim)
reálné adresáře odpovědět
ahoj, chci se zeptat, jak to dělat s opravdovými adresáři, když přepisuju adresy na např: stranky.cz/kategorie/strana/ (kde kategorie a strana nejsou doopravdy adresáře), a chci třeba stranky.cz/apliakce/, kde aplikace jsou opravdovým adresářem?

zkoušel jsem podle komentářů, kde radíš, jak toto nastavit pro soubory, udělat to samé i pro adresáře (tedy System.IO.Directory.Exists(....) ...), nicméně to nefunguje.

dík za radu
gravatar 10/27/2008 6:57:12 PM
[13] (Besi (roman(at)amenit.cz))
Obsluha všech koncovek souborů odpovědět
Nevíte prosím někdo jak toto nastavit v IIS7?
gravatar 11/9/2008 11:38:41 PM
[14] (Georgeek (vulcain(at)seznam.cz)) www
(vlastní) Stránka 404 odpovědět
Ahoj,

rád bych uvedl věc na pravou míru. Řešení existuje.

Stačí detekovat chybu a pokračovat např. kódem:

Server.Transfer("~/404.aspx");

, který provede přesměrování na straně serveru, bez účasti klienta, takže se url nezmění a chybová hlavička bude odeslána.

viz.: http://dotnetperls.com/Content/Server-Transfer-Use-ASPNET.aspx
 

TOPlist