Dotnetnuke (dále jen DNN) je jeden z rozšířených ASP.NET CMS webových systému. Výchozí URL adresy, které tento systém tvoří pro danou definici stránek a portálů ale neodpovídají SEO formátu (search engine optimization) výhodného pro internetové weby a aplikace. V tomto příspěvku uvedu moje řešení tohoto problému.
Základní URL adresa DNN stránek je tvořena např. takto:
http://www.imp.cz/tabid/70/Default.aspx
(http://www.imp.cz/tabid/70/Default.aspx?language=cs-CZ - pro více jazyčný web).
DNN Friendly Url Provider
Pro změnu URL adresy na jinou, než je fyzické umístění stránky slouží technologie obecně nazývaná URL Rewriting (přepisování URL adres). V DNN je na to jako součást obsažen modul UrlRewriteModule. Jeho nastavení je ve web configu v sekci modules:
<add name="UrlRewrite" type="DotNetNuke.HttpModules.UrlRewriteModule, DotNetNuke.HttpModules" preCondition="managedHandler" />
A dále je v configu obsažena sekce pro nastavení DNNFriendlyUrlProvider modulu:
<friendlyUrl defaultProvider="DNNFriendlyUrl">
<providers>
<clear />
<add name="DNNFriendlyUrl" type="DotNetNuke.Services.Url.FriendlyUrl.DNNFriendlyUrlProvider, DotNetNuke.HttpModules" includePageName="true" regexMatch="[^a-zA-Z0-9 _-]" />
</providers>
</friendlyUrl>
V jeho nastavení je uveden parametr includePageName="true", to způsobí, že adresy vypadají takto:
http://www.imp.cz/Ospolečnosti/Vznikahistorie/tabid/70/Default.aspx
(http://www.imp.cz/Ospolečnosti/Vznikahistorie/tabid/70/language/cs-CZ/Default.aspx - pro více jazyčný web)
Zhruba od DNN verze 4.6 je DNNFriendlyUrlProvider rozšířen o nové nastavení urlFormat="HumanFriendly", s jeho použití dostáváme tyto adresy:
http://www.imp.cz/Ospolečnosti/Vznikahistorie.aspx
(http://www.imp.cz/cs-cz/Ospolečnosti/Vznikahistorie.aspx - pro pro více jazyčný web)
Pozn.: Toto nastavení je od verze DNN 5.1.2 jako výchozí.
Toto URL má ale pořád ještě nedostatky:
- Diakritika – Pro URL DNN používá přímo název stránky, autoři asi nepředpokládají, že některé země mají v názvech i jiné znaky, angličtina tento problém nemá. Řešení že by se do názvu stránky zadal text bez diakritiky jen pro URL také nejde, protože název stránky se používá do textů v menu (a to také nelze změnit).
- Chybějící mezery – Z názvů stránek jsou automaticky vyhozeny mezery a jednotlivé slova jsou slepena. My v naší SEO URL mezery také nechceme, ale potřebujeme je místo toho zaměnit např. na znak pomlčky (‘-’).
Pozn.: Vnitřně v DNN je to dokonce tak, že tyto mezery jsou odstraněny již při zakládání stránky, kde se předpočítává string s cestou pro stránku, a ten je uložen to sloupce TapPath tabulky Tabs.
- První písmeno velkými písmeny – My požadujeme, aby celé URL bylo malými písmeny.
Tyto nedostatky budeme v další části článku odstraňovat, předtím ještě ale uvedu druhou možnost jak získat podobná “Friendly URL”.
iFinity.FriendlyUrlProvider
Kromě standartního FriendlyUrlProvidera existuje jiný provider (tuším že vznikal ještě předním než standartní provider podporoval nastavení HumanFriendly). Jedná se o iFinity FriendlyUrlProvider od Bruce Chapmana, je volně ke stažení včetně zdrojových kódů zde. (Jedná se o free část DNN rozšíření iFinity Url Master Module.)
Jeho nastavení může vypadat např. takto:
<add name="UrlRewrite" type="iFinity.DNN.Modules.FriendlyUrl.UrlRewriteModule, iFinity.FriendlyUrlProvider" preCondition="managedHandler" />
<friendlyUrl defaultProvider="iFinity.FriendlyUrl">
<providers>
<clear />
<add name="iFinity.FriendlyUrl" type="iFinity.DNN.Modules.FriendlyUrl.DNNFriendlyUrlProvider, iFinity.FriendlyUrlProvider" includePageName="true" regexMatch="[^\+a-zA-Z0-9 _-]" urlFormat="HumanFriendly" redirectUnfriendly="true" doNotRedirect="SearchResults;" checkForDupUrls="true" forceLowerCase="true" redirectWrongCase="true" replaceSpaceWith="-" logCacheMessages="false"/>
</providers>
</friendlyUrl>
Při jeho použití dostaneme podobné URL jako s HumanFriendly nastavením standartního providera, navíc ale tento provider umožňuje specifikovat některé další parametry jako například replaceSpaceWith="-" nastavuje znak, který je použít v URL místo mezery, nebo forceLowerCase="true" změní odkazy na malá písmena.
Výsledné URL nyní dostáváme následující:
http://www.imp.cz/o-společnosti/vznik-a-historie.aspx
(http://www.imp.cz/language/cs-cz/o-společnosti/vznik-a-historie.aspx - pro více jazyčný web)
Pozn.: iFinity.FriendlyUrlProvidera také obsahuje některé další funkce, např. standartní provider neumí správně pracovat s některými moduly DNN jako Blog modul, dále tento provider řeší problém 301 Redirection a další, více se o tom můžete dočíst v článku na blogu Bruce Chapmana zde.
Výsledkem jsou tedy podobná URL jako v prvním případě HumanFriendly URL, s použitím nastavení replaceSpaceWith a forceLowerCase jsme odstranili problém mezer a velikosti, ale pořád zůstává problém českých znaků.
SEOUrlProvider
Abychom odstranili všechny výše uvedené problémy URL musíme napsat vlastního FriendlyUrl providera. Ten kdo pozorně četl si jistě všiml, že jsem psal, že iFinity.FriendlyUrlProvider je možné stáhnou i se zdrojovými kódy, a na tom je právě založeno moje řešení, kterým je tento kód upravit, aby vyhovoval našim potřebám.
Změny provedeme v souboru FriendlyUrlProvider, který obsahuje třídy DNNFriendlyUrlProvider a TabPathHelper. Do třídy TabPathHelper přidáme novou metodu GetSEOString, která vrací jméno stránky v SEO formátu:
/// <summary>
/// Returns string in SEO format
/// </summary>
private static string GetSEOString(string str)
{
str = str.Normalize(System.Text.NormalizationForm.FormD);
var sb = new System.Text.StringBuilder();
for (int i = 0; i < str.Length; i++)
{
//Do řetězce přidá všechny znaky kromě modifikátorů
if (CharUnicodeInfo.GetUnicodeCategory(str[i]) != UnicodeCategory.NonSpacingMark)
{
sb.Append(str[i]);
}
}
sb.Replace((char)8211, '-');
sb.Replace(' ', '-');
sb.Replace("\r", "");
sb.Replace("\n", "");
sb.Replace("\t", "-");
sb.Replace(",", "");
sb.Replace(":", "");
sb.Replace("?", "");
sb.Replace("!", "");
sb.Replace('.', '-');
sb.Replace(";", "");
sb.Replace("@", "");
sb.Replace("\"", "");
sb.Replace("'", "");
sb.Replace("$", "");
sb.Replace("/", "");
string value = System.Text.RegularExpressions.Regex.Replace(sb.ToString(), "-+", "-");
int index;
while ((index = value.IndexOf("--")) != -1)
{
value.Replace("--", "-");
}
return value.ToLowerInvariant();
}
Nyní s využitím této metody změníme metody GetTabPath, BuildTabPath a AppendToTabPath.
/// <summary>
/// For the supplied options, return a tab path for the specified tab
/// </summary>
/// <param name="tab">TabInfo object of selected tab</param>
/// <param name="settings">FriendlyUrlSettings</param>
/// <param name="ignoreCustomRedirects">Whether to add in the customised Tab redirects or not</param>
/// <returns>The tab path</returns>
internal static string GetTabPath(TabInfo tab, FriendlyUrlSettings settings, bool ignoreCustomRedirects, bool homePageSiteRoot, int portalHomeTabId)
{
string newTabPath;
if (homePageSiteRoot && tab.TabID == portalHomeTabId)
{
newTabPath = "/"; //site root for home page
}
else
{
//build up tab path with space replacement in SEO format
newTabPath = BuildTabPath(tab).Replace("//", "/");
}
return newTabPath;
}
public static string BuildTabPath(TabInfo tab)
{
TabInfo parenttab = null;
string path = "";
if (tab.ParentId > -1)
{
DotNetNuke.Entities.Tabs.TabController tc = new TabController();
parenttab = tc.GetTab(tab.ParentId, tab.PortalID, false);
path = BuildTabPath(parenttab);
}
path = AppendToTabPath(path, tab);
return path;
}
private static string AppendToTabPath(string path, TabInfo tab)
{
return path + "/" + GetSEOString(tab.TabName);
}
S touto implementací budou již výsledná URL webu tak jak jsme na začátku požadovali, konkrétně:
http://www.imp.cz/o-spolecnosti/vznik-a-historie.aspx
(http://www.imp.cz/language/cs-cz/o-spolecnosti/vznik-a-historie.aspx - pro více jazyčný web)
Pozn.: Provider nyní bude vracet URL bez českých znaků, celé malými písmeny a místo mezer bude znak pomlčka (‘-’), všechny tyto záměny URL se provádějí vždy bez ohledu na nastavení parametrů replaceSpaceWith, forceLowerCase, které se nyní ignorují.
Visual Studio solution a Release verzi tohoto upraveného providera je možné stáhnout zde. Solution navíc obsahuje změnu jména třídy providera na SEOUrlProvider a jména assembly na SEOUrlProvider.dll. Patřičné změny nastavení Web.config pro tyto jména a assembly jsou tyto:
<httpModules>
<add name="UrlRewrite" type="iFinity.DNN.Modules.FriendlyUrl.UrlRewriteModule, SEOUrlProvider"/>
</httpModules>
<modules>
<add name="UrlRewrite" type="iFinity.DNN.Modules.FriendlyUrl.UrlRewriteModule, SEOUrlProvider" preCondition="managedHandler"/>
</modules>
<friendlyUrl defaultProvider="SEOUrlProvider">
<providers>
<clear />
<add name="SEOUrlProvider" type="iFinity.DNN.Modules.FriendlyUrl.SEOUrlProvider, SEOUrlProvider" includePageName="true" regexMatch="[^\+a-zA-Z0-9 _-]" urlFormat="HumanFriendly"
redirectUnfriendly="true" doNotRedirect="SearchResults;" checkForDupUrls="true" forceLowerCase="true" redirectWrongCase="true" replaceSpaceWith="-" logCacheMessages="false" />
</providers>
</friendlyUrl>
Non aspx extension
V předchozí části jsme změnili formát URL stránek, aby odpovídaly SEO formátu, ale zůstala v nich koncovka .aspx. Nyní ještě můžeme URL dále vylepšit tím, že tuto koncovku odstraníme. Důvodem pro to může mimo jiné být i to, že tím uživatelům skryjeme to v jaké technologii jsou naše stránky vytvořeny (tedy že jsou v ASP.NET).
První (nyní již jednoduchou části) je změna vlastního URL, k tomu využijeme nastavení iFinity.FriendlyUrlProvidera (a tedy i naší upravené verze SEOUrlProvider) pageExtensionUsage="never". Tím dostaneme URL bez přípony:
http://www.imp.cz/o-spolecnosti/vznik-a-historie
(http://www.imp.cz/language/cs-cz/o-spolecnosti/vznik-a-historie - pro více jazyčný web)
Po této změně ale přestanou stránky s těmito URL nabíhat, to je způsobené tím, že defaultně IIS zpracovává ASP.NET požadavky pouze pro ASP.NET přípony souborů (např.: .aspx, .ashx atd.). Řešení tohoto problému je různé, podle toho jakou verzi IIS používáme. Pro staré IIS 6.0 (nebo IIS 7.0 v classic režimu), je potřeba ASP.Net ISAPI filter přiřadit pro všechny requesty webu, popisů lze najít celou řadu, stručný návod je uveden i ve výše zmíněném článku Bruce Chapmana zde. Tím se tady více zabývat nebudu, ale naopak se chci zaměřit na případ pro integrovaný managed pipeline režim v IIS 7.0/7.5, který moc často popisovaný nebývá.
V IIS 7.0/7.5 je od předchozích verzí změněna architektura, v integrated modu se všechny požadavky vykonávají použití managed kódu, bez ohledu na přípony souborů. Tak proč požadavky z našich URL bez přípon na IIS 7 rovnou sami nechodí? Problém je v tom, že při registraci managed modulů, které zpracovávaní requesty je uvedena podmínka preCondition managedHandler na jejich spouštění.
preCondition obecně slouží k určení, že modul bude natažen např. pouze pro danou architekturu procesoru (x86, x64), pro určitou runtime verzi .NET, nebo pouze v classic modu - ISAPIMode (ke každému modulu lze určit více hodnot). Nás ale zajímá speciální hodnota managedHandler, ta určuje, že se modul spustí pouze pokud je požadavek mapován na handler běžící v managed kódu, což se (v sekci handlers) nastavuje pořád typicky podle přípon (tedy např. .aspx se zpracovává managed hadlerem - PageHandlerFactory a třeba takové .html ne).
V našem případě tedy nejsou potřebné moduly díky podmínce managedHandler spouštěný. Změnit nastavení modulů můžeme provést dvěma způsoby:
- V sekci modules nastavit atribut runAllManagedModulesForAllRequests="true", tím budou spouštěny všechny managed moduly pro všechny requesty bez ohledu na typu handlerů.
nebo
- a) Pro každý modul hodnotu managedHandler z preCondition atributu odstraníme, tj. pokud není v atributu uvedena jiná podmínka, tak preCondition nastavíme na prázdnou hodnotu (nebo atribut nebudeme uvádět).
b) Pokud je modul definován v některém z nadřazených Web.config nebo v Machine.config, musíme modul nejprve odstranit definicí remove a poté znovu zaregistrovat pomoci add definice (bez preCondition).
Já jsem zvolil tu méně drastickou metodu a změnil pouze potřebné moduly, mé nastavení modulů v dotnetnuke Web.config vypadá následovně:
<system.webServer>
<modules>
<remove name="FormsAuthentication" />
<add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" preCondition="" />
<remove name="DefaultAuthentication" />
<add name="DefaultAuthentication" type="System.Web.Security.DefaultAuthenticationModule" preCondition="" />
<add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" preCondition="" />
<add name="Compression" type="DotNetNuke.HttpModules.Compression.CompressionModule, DotNetNuke.HttpModules" preCondition="" />
<add name="RequestFilter" type="DotNetNuke.HttpModules.RequestFilter.RequestFilterModule, DotNetNuke.HttpModules" preCondition="" />
<add name="UrlRewrite" type="iFinity.DNN.Modules.FriendlyUrl.UrlRewriteModule, SEOUrlProvider" preCondition="" />
<add name="Exception" type="DotNetNuke.HttpModules.Exceptions.ExceptionModule, DotNetNuke.HttpModules" preCondition="" />
<add name="UsersOnline" type="DotNetNuke.HttpModules.UsersOnline.UsersOnlineModule, DotNetNuke.HttpModules" preCondition="" />
<add name="DNNMembership" type="DotNetNuke.HttpModules.Membership.MembershipModule, DotNetNuke.HttpModules" preCondition="" />
<add name="Personalization" type="DotNetNuke.HttpModules.Personalization.PersonalizationModule, DotNetNuke.HttpModules" preCondition="" />
<add name="Analytics" type="DotNetNuke.HttpModules.Analytics.AnalyticsModule, DotNetNuke.HttpModules" preCondition="" />
<add name="RadUploadModule" type="Telerik.Web.UI.RadUploadHttpModule, Telerik.Web.UI" preCondition="" />
</modules>
</system.webServer>
(Moduly FormsAuthentication a DefaultAuthentication bylo potřeba odregistrovat a poté zaregistrovat znovu.)
O problematice preCondition také doporučuji přečíst si tento článek z www.ASPNET.cz.
Ať už v případě IIS 6.0 nebo IIS 7.0 je nyní náš UrlProvider na změnu URL spouštěn pro každý request, což je zbytečné v případě dokumentů ve formátů např.: .png, .gif, .jpg, .css, .js, .html apod. Proto ještě pomoci atributu ignoreFileTypesRegex UrlProvidera nastavíme regulární výraz pro ignorování Rewritingu pro tyto a podobné requesty. Celé nastavení vypadá následovně:
<friendlyUrl defaultProvider="SEOUrlProvider">
<providers>
<clear />
<add name="SEOUrlProvider" type="iFinity.DNN.Modules.FriendlyUrl.SEOUrlProvider, SEOUrlProvider" includePageName="true" regexMatch="[^\+a-zA-Z0-9 _-]" urlFormat="HumanFriendly"
redirectUnfriendly="true" pageExtensionUsage="never" ignoreFileTypesRegex="(?<!linkclick\.aspx.+)(?:\.pdf$|\.gif$|\.png($|\?)|\.css$|\.js($|\?)|\.jpg$|\.html$|\.htm$|\.axd($|\?)|\.swf$|\.flv$|\.ico$|\.xml($|\?)|.\asp($|\?)|/Downloads($|/|\?))"
doNotRedirect="SearchResults;" checkForDupUrls="true" forceLowerCase="true" redirectWrongCase="true" replaceSpaceWith="-" logCacheMessages="false" />
</providers>
</friendlyUrl>
Pozn: V nastavení ignoreFileTypesRegex je ještě na konci uvedena další výjimka Downloads - jedná se o podsložku na našem webu, u které také chceme zakázat změnu URL. O nastavení ignoreFileTypesRegex jsem našel následující příspěvky na iFinity forum zde a zde.