O que quer dizer conteúdo duplicado?
Conteúdo duplicado é o mesmo conteúdo repetido em várias páginas/sites. Isto não parece um grande problema, mas não é algo bom para a otimização de buscas. O Google não quer que os resultados das buscas mostrem o mesmo conteúdo várias vezes através de diferentes sites, então eles penalizam ou escondem o conteúdo duplicado. Se você tem duas páginas com o mesmo conteúdo, você pode dividir os links em dois, reduzindo o pagerank.
O que isso tem a ver com o ASP.NET.MVC?
Infelizmente no ASP.NET MVC é muito fácil ter o mesmo conteúdo indexado várias vezes. Abaixo temos uma lista dos principais problemas:
- Case-sensitivity: URLs no ASP.NET(ou melhor, IIS e Windows), não são case sensitives. Isto significa que você pode escrever Default.asp, default.asp ou mesmo DeFauLt.aSp e continuará sendo a mesma página. Enquanto este caso ocorrer dentro do seu website, não será difícil alguém criar links com diferentes formas de escrita para o seu website (por exemplo: alguém pode deixar o caps lock ligado).
- Documentos padrões: muitos websites têm um documento padrão configurado para servir quando um nome de arquivo não é fornecido na requisição, por exemplo: http://mydomain.com/ pode ser atualmente http://mydomain.com/default.asp, mas isto não é informado ao browser, portanto fará com que sejam reconhecidas duas URLs diferentes.
- Barras à direita: enquanto os problemas acima são questões comuns do ASP.NET/IIS, barras à direita serão algo que só virão a se tornar problema do MVC por causa do seu roteamento. No ASP.NET se você solicitou http://mydomain.com/arquivos e você tinha uma pasta chamada arquivos, o IIS iria emitir um redirecionamento para mydomain.com/aquivos/. No entanto, no ASP.NET MVC o roteamento de URL vai tratar de colocar barras à direita para uma requisição sem essas barras. Então, http://mydomain.com/controller/action é exatamente a mesma coisa que http://mydomain.com/controller/action/ e o resultado disso é conteúdo duplicado.
- Query Strings: Query Strings podem ser um grande problema para duplicar conteúdo. Imagine se você puder adicionar “?sort=field“ no final da sua página para ter uma ordenação. Para o motor de busca isto é como se fosse outra página, mas o conteúdo é basicamente o mesmo. Felizmente, ASP.NET MVC não faz uso de query strings graças ao excelente roteamento URL.
Então, o que nós podemos fazer?
Lowercase URLs
Nós podemos forçar todas as requisições da nossa aplicação para serem em letra minúscula, através do método BeginRequest no Global.asax e redirecionando para uma versão em letra minúscula, mesmo se elas estiverem parcialmente ou totalmente em letras maiúsculas, segue abaixo o código:
protected void Application_BeginRequest(Object sender, EventArgs e)
{
//Aqui pegamos a URL da requisição e fazemos algumas validações.
//Nós exluímos a query string e adicionamos depois, e então isso não é incluído na validação.
string url = (Request.Url.Scheme + "://" + HttpContext.Current.Request.Url.Authority + HttpContext.Current.Request.Url.AbsolutePath);
// Se tiver caracteres maiúsculos, corrigir.
if (Regex.IsMatch(url, @"[A-Z]")){
PermanentRedirect(url.ToLower() + HttpContext.Current.Request.Url.Query);
}
/// <summary>
/// Redireciona com um cabeçalho de 301 a passar ao longo de qualquer valor de entrada PageRank/link.
/// </summary>
/// <param name="url">URL para redirecionamento.</param>
private void PermanentRedirect(string url)
{
Response.Clear();
Response.Status = "301 Moved Permanently";
Response.AddHeader("Location", url);
Response.End();
}
}
Dessa forma, se qualquer um que requisitar uma URL que contenha caracteres maiúsculos, será redirecionado com um 301 redirect. Isto funciona muito bem, mas nós temos um problema. Toda URL gerada internamente pelo MVC irá continuar usando nomes de Actions e Controllers em Pascal case (assumindo que assim são chamadas as suas classes).
Isto significa que todo link contido no seu website irá causar duas requisições (a primeira inicia no redirect). Para consertar isso, nós iremos fazer um override para o comportamento padrão para criação de URLs. Nós iremos criar uma rota, e essa rota irá criar uma instância de uma nova classe, chamada LowercaseRoute. Esta classe terá um override do método GetVirtualPath, que fará a conversão da URL para letras minúsculas antes de retorná-la.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Routing;
using System.Web.Mvc;
namespace Site.App_Start
{
public class LowercaseRoute : System.Web.Routing.Route
{
public LowercaseRoute(string url, IRouteHandler routeHandler)
: base(url, routeHandler) { }
public LowercaseRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
: base(url, defaults, routeHandler) { }
public LowercaseRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
: base(url, defaults, constraints, routeHandler) { }
public LowercaseRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler)
: base(url, defaults, constraints, dataTokens, routeHandler) { }
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData path = base.GetVirtualPath(requestContext, values);
if (path != null)
path.VirtualPath = path.VirtualPath.ToLowerInvariant();
return path;
}
}
public static class RouteCollectionExtensions
{
public static void MapRouteLowercase(this RouteCollection routes, string name, string url, object defaults)
{
routes.MapRouteLowercase(name, url, defaults, null);
}
public static void MapRouteLowercase(this RouteCollection routes, string name, string url, object defaults, object constraints)
{
if (routes == null)
throw new ArgumentNullException("routes");
if (url == null)
throw new ArgumentNullException("url");
var route = new LowercaseRoute(url, new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(defaults),
Constraints = new RouteValueDictionary(constraints)
};
if (String.IsNullOrEmpty(name))
routes.Add(route);
else
routes.Add(name, route);
}
}
}
Você pode colocar essas classes em qualquer lugar. Porque MapRouteLowercase é um método de extensão, você pode simplesmente chamá-lo na classe RouteCollection no lugar da chamada MapRoute existente em seu Global.asax.
// Home stuff
routes.MapRouteLowercase(
"Default",
"{page}",
new { controller = "Home", action = "Index", page = 1 },
new { page = @"\d+" }
);
Documentos padrões
Enquanto esse problema não afeta o MVC da mesma forma, aqui temos um problema similar. No ASP.NET MVC o roteamento padrão é: {controller}/{action} mas é configurado por padrão a action do index. Isso significa que em um projeto recentemente criado, ambos /home/index e /home terão o mesmo conteúdo. Para contornar esse problema, e fornecer algumas URLs mais agradáveis, eu mudei o encaminhamento um pouco, para que minha action padrão mapeada para a raiz siga um percurso separado da homepage (que aceita páginas, para permitir a navegação para posts antigos).
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Posts
routes.MapRouteLowercase(
"Posts",
"posts/{url}",
new { controller = "Post", action = "Display" }
);
// Tags
routes.MapRouteLowercase(
"Tags",
"tags/{url}/{page}",
new { controller = "Tag", action = "Display", page = 1 },
new { page = @"\d+" }
);
// Home stuff
routes.MapRouteLowercase(
"Default",
"{page}",
new { controller = "Home", action = "Index", page = 1 },
new { page = @"\d+" }
);
// Home stuff
routes.MapRouteLowercase(
"Home",
"{action}",
new { controller = "Home", action = "" }
);
// Catch-all for any unmatched URL
routes.MapRouteLowercase(
"Error Catch-All",
"{*path}",
new { controller = "Home", action = "NotFound" } // NotFound doesn't exist, so HandleUnknownAction will be fired
);
}
Barras à direita
Para evitar barras à direita e algumas outras questões menores (tais como as pessoas adicionando / 1 para uma URL para obter página 1, que é servida sem o / 1) eu adicionei algumas regras adicionais para o meu Global.asax, como abaixo:
protected void Application_BeginRequest(Object sender, EventArgs e)
{
//Aqui pegamos a URL da requisição e fazemos algumas validações.
//Nós exluímos a query string e adicionamos depois, e então isso não é incluído na validação.
string url = (Request.Url.Scheme + "://" + HttpContext.Current.Request.Url.Authority + HttpContext.Current.Request.Url.AbsolutePath);
// If we're not a request for the root, and end with a slash, strip it off
if (HttpContext.Current.Request.Url.AbsolutePath != "/" && HttpContext.Current.Request.Url.AbsolutePath.EndsWith("/"))
PermanentRedirect(url.Substring(0, url.Length - 1) + HttpContext.Current.Request.Url.Query);
// If we end with /1 we're a page 1, and don't need (shouldn't have) the page number
if (HttpContext.Current.Request.Url.AbsolutePath.EndsWith("/1"))
PermanentRedirect(url.Substring(0, url.Length - 2) + HttpContext.Current.Request.Url.Query);
// If we have double-slashes, strip them out
else if (HttpContext.Current.Request.Url.AbsolutePath.Contains("//"))
PermanentRedirect(url.Replace("//", "/") + HttpContext.Current.Request.Url.Query);
// Se tiver caracteres maiúsculos, irá corrigir.
else if (Regex.IsMatch(url, @"[A-Z]"))
PermanentRedirect(url.ToLower() + HttpContext.Current.Request.Url.Query);
}
Espero que este artigo tenha sido útil. Vejo vocês na próxima!
***
Artigo original disponível em: http://blog.dantup.com/2009/04/reducing-duplicate-content-with-aspnet.html







