I'm getting myself acquainted with ASP.Net-MVC, and I was trying to accomplish some common tasks I've accomplished in the past with webforms and other functionality. On of the most common tasks I need to do is create SEO-friendly urls, which in the past has meant doing some url rewriting to build the querystring into the directory path.
for example:
www.somesite.com/productid/1234/widget
rather than:
www.somesite.com?productid=1234&name=widget
What method do I use to accomplish this in ASP.Net-MVC?
I've search around, and all I've found is this, which either I'm not understanding properly, or doesn't really answer my question:
SEO URLs with ASP.NET MVC
MVC stands for "Model View Controller" and while those concepts aren't what you're asking about, you generally can wire up URL's like you see above quite easily
So for example by default the URL's look like the following
http://www.somesite.com/controller/view/
where controller refers to the controller class within your project, and view refers to the page/method combination within the controller. So for example you could write the view to take in an input and look something like the following
http://www.somesite.com/widget/productid/1234/
Now as for SEO Friendly URL's, that's just useless sugar. You author your controller such that it adds harmless cruft to the end of the URL.
So for example, you'll notice that the following three ways to get to this question produce the same result:
How do I create SEO-Friendly urls in ASP.Net-MVC
How do I create SEO-Friendly urls in ASP.Net-MVC
How do I create SEO-Friendly urls in ASP.Net-MVC
Stack Overflow has authored their route values such that the bit that occurs after the question ID isn't really necessary to have.
So why have it there? To increase Google PageRank. Google PageRank relies on many things, the sum total of which are secret, but one of the things people have noticed is that, all other things being equal, descriptive text URL's rank higher. So that's why Stack Overflow uses that text after the question number.
Create a new route in the Global.asax to handle this:
routes.MapRoute(
"productId", // Route name
"productId/{id}/{name}", // URL with parameters
new { controller = "Home", action = "productId", id = 1234, name = widget } // Parameter defaults
);
Asp.Net MVC has routing built in, so no need for the Url Rewriter.
Be careful when implementing routes with names in them, you need to validate that the name coming in is correct or you can end up harming your SEO-Ranking on the page by having multiple URIs share the same content, either set up a proper canonical or have your controller issue 301s when visiting the 'wrong' URI.
A quick writeup I did on the latter solution can be found at:
http://mynameiscoffey.com/2010/12/19/seo-friendly-urls-in-asp-net-mvc/
Some info on canonicals:
http://googlewebmastercentral.blogspot.com/2009/02/specify-your-canonical.html
I think what you are after is MVC Routing
have a look at these
MVC Routing on www.asp.net
Book: ASP.NET MVC in Action - Routing
It is also important to handle legacy urls. I have a database of old and new urls and I redirect with the following code;
public class AspxCatchHandler : IHttpHandler, IRequiresSessionState
{
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
if (context.Request.Url.AbsolutePath.Contains("aspx") && !context.Request.Url.AbsolutePath.ToLower().Contains("default.aspx"))
{
string strurl = context.Request.Url.PathAndQuery.ToString();
string chrAction = "";
string chrDest = "";
try
{
DataTable dtRedirect = SqlFactory.Execute(
ConfigurationManager.ConnectionStrings["emptum"].ConnectionString,
"spGetRedirectAction",
new SqlParameter[] {
new SqlParameter("#chrURL", strurl)
},
true);
chrAction = dtRedirect.Rows[0]["chrAction"].ToString();
chrDest = dtRedirect.Rows[0]["chrDest"].ToString();
chrDest = context.Request.Url.Host.ToString() + "/" + chrDest;
chrDest = "http://" + chrDest;
if (string.IsNullOrEmpty(strurl))
context.Response.Redirect("~/");
}
catch
{
chrDest = "/";// context.Request.Url.Host.ToString();
}
context.Response.Clear();
context.Response.Status = "301 Moved Permanently";
context.Response.AddHeader("Location", chrDest);
context.Response.End();
}
else
{
string originalPath = context.Request.Path;
HttpContext.Current.RewritePath("/", false);
IHttpHandler httpHandler = new MvcHttpHandler();
httpHandler.ProcessRequest(HttpContext.Current);
HttpContext.Current.RewritePath(originalPath, false);
}
}
#endregion
}
hope this is useful
i think stackoverflow is best practice.
because when you remove a word at end of url, it redirect with 301 status to current url.
domain.com/product/{id}/{slug}
it had some benefits:
when you change slug of page it will redirect to correct url
your url is short but have seo friendly words at end.
in backend you use id to find product but you have slug for search engine
you can have same slug for different page. I now its not good for seo but you can have same slug for 2 products
the code will be like #Martin
routes.MapRoute(
name: "Cafes",
url: "cafe/{id}/{FriendlyUrl}",// how send array in metod get
defaults: new { controller = "cafe", action = "index", id = UrlParameter.Optional, FriendlyUrl = UrlParameter.Optional }//, id = UrlParameter.Optional
);
in action controller you should check database slug of product with the parameter that you receive. if they are not same redirect with status 301 to correct url.
the code for redirect:
return RedirectToActionPermanent("index", new
{
id = CafeParent.CafeID,
FriendlyUrl = CafeParent.CafeSlug.Trim()
});
RedirectToActionPermanent redirect with status 301. when you do this, the search engine find that this url is changed to what you redirected.
Related
I am working on a mvc core 3.1 application. Seo requirements are to show product name with main site instead of complete url.
My original url is
www.abc.com/Fashion/ProductDetail?productId=5088&productName=AliviaBlack&brandId=3
Requirement are
www.abc.com/alivia-black
I have tried following by using attribute routing
endpoints.MapControllerRoute(
name: "SpecificRoute",
pattern: "/{productName}",
defaults: new { controller = "Fashion", action = "ProductDetail", id= "{productId}" });
In view page
<a asp-route-productId="#product.ProductId"
asp-route-productName="#Common.FilterURL(product.ProductName)"
asp-route-brandId="#product.FashionBrand.FashionBrandId" asp-route="SpecificRoute">
Result is
www.abc.com/alivia-black?productId=5223&brandId=4
How to remove question mark and parameters after it.
First off, URIs need to be resilient. You say your current requirement is to have URIs like this:
www.abc.com/alivia-black
i.e.:
{host}/{productName}
That's a very bad URI template because:
It does not uniquely identify the product (as you could have multiple products with the same name).
It will break existing links from external websites if you ever rename a product or replace a product with the same name. And this happens a lot in any product database.
Because you're putting the {productName} in the "root" of your URI structure it means it's much harder to handle anything else besides viewing products (e.g. how would you have a /contact-us page? What if you had a product that was named contact-us?)
I stress that is is very important to include an immutable key to the entity being requested (in this case, your productId value) in the URI and use that as a primary-reference, so the productName can be ignored when handling an incoming HTTP request. This is how StackOverflow's and Amazon's URIs work (you can trim off the text after a StackOverflow's question-id and it will still work: e.g.
https://stackoverflow.com/questions/69748993/how-to-show-seo-friendly-url-in-mvc-core-3-1
https://stackoverflow.com/questions/69748993
I strongly recommend you read this article on the W3.org's website all about designing good URIs, as well as other guidance from that group's homepage.
I suggest you use a much better URI template, such as this one:
{host}/products/{productId}/{productName}
Which will give you a URI like this:
abc.com/products/5088/alivablack
Handling such a link in ASP.NET MVC (and ASP.NET Core) is trivial, just set the route-template on your controller action:
[Route( "/products/{productId:int}/{productName?}" )]
public async Task<IActionResult> ShowProduct( Int32 productId, String? productName = null )
{
// Use `productId` to lookup the product.
// Disregard `productName` unless you want to use it as a fallback to search your database if `productId` doesn't work.
}
As for generating URIs, as I recommend against using TagHelpers (e.g. <a asp-route-) because they don't give you sufficient control over how the URI is rendered, instead you can define a UrlHelper extension method (ensure you #import the namespace into your .cshtml pages (or add it to ViewStart):
public static class MyUrls
{
public static String ShowProduct( this IUrlHelper u, Int32 productId, String productName )
{
const String ACTION_NAME = nameof(ProductsController.ShowProduct);
const String CONTROLLER_NAME = "Products"; // Unfortunately we can't use `nameof` here due to the "Controller" suffix in the type-name.
String url = u.Action( action: ACTION_NAME, controller: CONTROLLER_NAME, values: new { productId = productId, productName = productName } );
return url;
}
}
Then you can use it like so:
View AliviaBlack
You can also make ShowProduct accept one of your Product objects directly and then pass the values on to the other overload (defined above) which accepts scalars:
public static String ShowProduct( this IUrlHelper u, Product product )
{
String url = ShowProduct( u, productId: product.ProductId, productName: product.ProductName );
return url;
}
Then you can use it like so (assuming product is in-scope):
#product.ProductName
It would be great if someone could help me on this problem as I am new to web development and ASP.NET MVC. My goal is to design an MVC application which could help users to create their own mini websites. Each user will get their own mini websites and an admin panel to change the pages and template of their sites. I think there are scripts out there to achieve something similar, but we need to create our own for our specific requirements
Towards that goal I created a main mvc 3 application and I created an area as Site under it. We want the user to have their own sub domain urls like www.site1.mainsite.com where site1 is the name of the mini site of the user. For that I added 2 routes one for the main site and another for the area like
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "MainSite.UI.Controllers" }
);
In Area Registration
context.MapRoute(
"Sites_default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new {controller = new SiteConstraint()},
new[] { "MainSite.UI.Areas.Site.Controllers" });
Both of them have the same url type, but area route have a constraint where I added like
var url = httpContext.Request.Headers["HOST"];
var storeName = url.Split('.')[1];
var match = storeName != "mainsite";
return match;
so far its working, But i dont think its a good design. Now apart from this I need to add 2 more areas one is siteadmin and another is blog.
What should be best way to achieve this?
sub domain urls should be directed to area "site"
subdomain url/Admin should be routed to area "siteadmin"
domain/blog should be routed to blog area.
domain other urls should be handled by main controllers
Thanks in advance
I didnt get even one reply :( But I tried playing with the routes and achieved some what near to what I was looking for.
Thanks to Route Debugger, It helped me to see what was going wrong with the routes
When I tried adding another area "Admin", and registered it with the route below
context.MapRoute(
"SiteAdmin_default",
"Admin/{controller}/{action}/{id}",
new { controller = "Dashboard", action = "Index", id = UrlParameter.Optional },
new { controller = new SiteConstraint() },
new[] { "MainSite.UI.Areas.SiteAdmin.Controllers" }
);
Things started going wrong. Route Debugger helped to me to figure out, what was happening.
For me the order of the Routes really mattered, so I had to make sure that my areas were registered in the correct order.
AreaRegistration.RegisterAllAreas()
was not registering my routes in the correct order, Googling gave me this helper method.
public static class RouteHelper
{
public static void RegisterArea<T>(RouteCollection routes, object state) where T : AreaRegistration
{
var registration = (AreaRegistration)Activator.CreateInstance(typeof(T));
var context = new AreaRegistrationContext(registration.AreaName, routes, state);
var tNamespace = registration.GetType().Namespace;
if (tNamespace != null)
{
context.Namespaces.Add(tNamespace + ".*");
}
registration.RegisterArea(context);
}
}
That method helped me to register the areas in the required order in the Global asax as
RouteHelper.RegisterArea<SiteAdminAreaRegistration>(RouteTable.Routes, null);
RouteHelper.RegisterArea<SiteAreaRegistration>(RouteTable.Routes, null);
I am writing it down, what I got so far as an answer so that someone else may be find it useful.
We're currently running on IIS6, but hoping to move to IIS 7 soon.
We're moving an existing web forms site over to ASP.Net MVC. We have quite a few legacy pages which we need to redirect to the new controllers. I came across this article which looked interesting:
http://blog.eworldui.net/post/2008/04/ASPNET-MVC---Legacy-Url-Routing.aspx
So I guess I could either write my own route handler, or do my redirect in the controller. The latter smells slightly.
However, I'm not quite sure how to handle the query string values from the legacy urls which ideally I need to pass to my controller's Show() method. For example:
Legacy URL:
/Artists/ViewArtist.aspx?Id=4589
I want this to map to:
ArtistsController Show action
Actually my Show action takes the artist name, so I do want the user to be redirected from the Legacy URL to /artists/Madonna
Thanks!
depending on the article you mentioned, these are the steps to accomplish this:
1-Your LegacyHandler must extract the routes values from the query string(in this case it is the artist's id)
here is the code to do that:
public class LegacyHandler:MvcHandler
{
private RequestContext requestContext;
public LegacyHandler(RequestContext requestContext) : base(requestContext)
{
this.requestContext = requestContext;
}
protected override void ProcessRequest(HttpContextBase httpContext)
{
string redirectActionName = ((LegacyRoute) RequestContext.RouteData.Route).RedirectActionName;
var queryString = requestContext.HttpContext.Request.QueryString;
foreach (var key in queryString.AllKeys)
{
requestContext.RouteData.Values.Add(key, queryString[key]);
}
VirtualPathData path = RouteTable.Routes.GetVirtualPath(requestContext, redirectActionName,
requestContext.RouteData.Values);
httpContext.Response.Status = "301 Moved Permanently";
httpContext.Response.AppendHeader("Location", path.VirtualPath);
}
}
2- you have to add these two routes to the RouteTable where you have an ArtistController with ViewArtist action that accept an id parameter of int type
routes.Add("Legacy", new LegacyRoute("Artists/ViewArtist.aspx", "Artist", new LegacyRouteHandler()));
routes.MapRoute("Artist", "Artist/ViewArtist/{id}", new
{
controller = "Artist",
action = "ViewArtist",
});
Now you can navigate to a url like : /Artists/ViewArtist.aspx?id=123
and you will be redirected to : /Artist/ViewArtist/123
I was struggling a bit with this until I got my head around it. It was a lot easier to do this in a Controller like Perhentian did then directly in the route config, at least in my situation since our new URLs don't have id in them. The reason is that in the Controller I had access to all my repositories and domain objects. To help others this is what I did:
routes.MapRoute(null,
"product_list.aspx", // Matches legacy product_list.aspx
new { controller = "Products", action = "Legacy" }
);
public ActionResult Legacy(int catid)
{
MenuItem menuItem = menu.GetMenuItem(catid);
return RedirectPermanent(menuItem.Path);
}
menu is an object where I've stored information related to menu entries, like the Path which is the URL for the menu entry.
This redirects from for instance
/product_list.aspx?catid=50
to
/pc-tillbehor/kylning-flaktar/flaktar/170-mm
Note that RedirectPermanent is MVC3+. If you're using an older version you need to create the 301 manually.
I am having problems with thinking up a solution for the following. I got a blog which I recently upgraded from web forms to MVC. The blog is avalible in both swedish and english on two different domains and are running in the same web site in IIS.
The problem is that I would like language specific urls on the both sites, like this:
English: http://codeodyssey.com/archive/2009/1/15/code-odyssey-the-next-chapter
Swedish: http://codeodyssey.se/arkiv/2009/1/15/code-odyssey-nasta-kapitel
At the moment I have made this to work by registering the RouteTable on each request depending on which domain is called. My Global.asax Looks something like this (not the whole code):
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
string archiveRoute = "archive";
if (Thread.CurrentThread.CurrentUICulture.ToString() == "sv-SE")
{
archiveRoute = "arkiv";
}
routes.MapRoute(
"BlogPost",
archiveRoute+"/{year}/{month}/{day}/{slug}",
new { controller = "Blog", action = "ArchiveBySlug" }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
routes.MapRoute(
"404-PageNotFound",
"{*url}",
new { controller = "Error", action = "ResourceNotFound" }
);
}
void Application_BeginRequest(object sender, EventArgs e)
{
//Check whcih domian the request is made for, and store the Culture
string currentCulture = HttpContext.Current.Request.Url.ToString().IndexOf("codeodyssey.se") != -1 ? "sv-SE" : "en-GB";
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(currentCulture);
Thread.CurrentThread.CurrentUICulture = new CultureInfo(currentCulture);
RouteTable.Routes.Clear();
RegisterRoutes(RouteTable.Routes);
Bootstrapper.ConfigureStructureMap();
ControllerBuilder.Current.SetControllerFactory(
new CodeOdyssey.Web.Controllers.StructureMapControllerFactory()
);
}
protected void Application_Start()
{
}
This works at the moment but I know it not a great solution. I have been getting a "Item has already been added. Key in dictionary" error when stating up this app and it does not seems stable at times.
I would like to only set up my routes in the Application_Start as they should and not having to clear them on every request like I am doing now. Problem is that the request object does not exist and I have no way of knowing which of the language specific routes I should register.
Been reading about the AppDomain but could not find many examples on how to use it on a web site. I'we been thinking to star something like this:
protected void Application_Start()
{
AppDomain.CreateDomain("codeodyssey.se");
AppDomain.CreateDomain("codeodyssey.com");
}
Then registring each web sites routes in each app domain and send the requests to one of them based on the url. Can't find any examples on how to work with AppDomains in this manner.
Am I completely off track? Or is there a better solution for this?
The ASP.Net runtime manages AppDomains for you, so its probably not a good idea to create AppDomains in your code.
However, if you can, I would suggest creating multiple IIS Applications (one for http://codeodyssey.com and one for http://codeodyssey.se). Point both applications at the same directory on disk. This will give you the two AppDomains you are looking for.
Then, in your Application_Start code, you can check the domain and build routes accordingly.
I want to make an MVC route for a list of news, which can be served in several formats.
news -> (X)HTML
news.rss -> RSS
news.atom -> ATOM
Is it possible to do this (the more general "optional extension" situation crops up in several places in my planned design) with one route? Or do I need to make two routes like this:
routes.MapRoute("News-ImplicitFormat",
"news",
new { controller = "News", action = "Browse", format = "" });
routes.MapRoute("News-ExplicitFormat",
"news.{format}"
new { controller = "News", action = "Browse" });
It seems like it would be useful to have the routing system support something like:
routes.MapRoute("News",
"news(.{format})?",
new { controller = "News", action = "Browse" });
I made a method to support adding pairs like this as follows:
public static void MapRouteWithOptionalFormat(this RouteCollection routes,
string name,
string url,
object defaults)
{
Route implicitRoute = routes.MapRoute(name + "-ImplicitFormat",
url,
defaults);
implicitRoute.Defaults.Add("format", string.Empty);
Route explicitRoute = routes.MapRoute(name + "-ExplicitFormat",
url + ".{format}",
defaults);
}
You can look into using constraints to make this work with normal routes.
UPDATE: actually, I misread the question. The other answer is the correct thing to do for now. Or create a custom route. We're looking at the idea of optional segments as a possible future feature.