I would like to create custom slugs for pages in my CMS, so users can create their own SEO-urls (like Wordpress).
我想在我的CMS中为页面创建自定义slug,因此用户可以创建自己的SEO-urls(如Wordpress)。
I used to do this in Ruby on Rails and PHP frameworks by "abusing" the 404 route. This route was called when the requested controller could not be found, enabling me te route the user to my dynamic pages controller to parse the slug (From where I redirected them to the real 404 if no page was found). This way the database was only queried to check the requested slug.
我以前通过“滥用”404路由在Ruby on Rails和PHP框架中执行此操作。无法找到请求的控制器时调用此路由,使我能够将用户路由到我的动态页面控制器以解析slug(如果没有找到页面,我将其重定向到真正的404)。这样,仅查询数据库以检查所请求的段塞。
However, in MVC the catch-all route is only called when the route does not fit the default route of /{controller}/{action}/{id}
.
但是,在MVC中,只有当路由不符合/ {controller} / {action} / {id}的默认路由时,才会调用catch-all路由。
To still be able to parse custom slugs I modified the RouteConfig.cs
file:
为了仍然能够解析自定义slugs我修改了RouteConfig.cs文件:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
RegisterCustomRoutes(routes);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { Controller = "Pages", Action = "Index", id = UrlParameter.Optional }
);
}
public static void RegisterCustomRoutes(RouteCollection routes)
{
CMSContext db = new CMSContext();
List<Page> pages = db.Pages.ToList();
foreach (Page p in pages)
{
routes.MapRoute(
name: p.Title,
url: p.Slug,
defaults: new { Controller = "Pages", Action = "Show", id = p.ID }
);
}
db.Dispose();
}
}
This solves my problem, but requires the Pages
table to be fully queried for every request. Because a overloaded show method (public ViewResult Show(Page p)
) did not work I also have to retrieve the page a second time because I can only pass the page ID.
这解决了我的问题,但需要为每个请求完全查询Pages表。因为重载的show方法(公共ViewResult Show(页面p))不起作用,我还必须第二次检索页面,因为我只能传递页面ID。
- Is there a better way to solve my problem?
- Is it possible to pass the Page object to my Show method instead of the page ID?
有没有更好的方法来解决我的问题?
是否可以将Page对象传递给我的Show方法而不是页面ID?
2 个解决方案
#1
2
Even if your route registration code works as is, the problem will be that the routes are registered statically only on startup. What happens when a new post is added - would you have to restart the app pool?
即使您的路由注册代码按原样工作,问题仍然是路由仅在启动时静态注册。添加新帖子后会发生什么 - 您是否需要重新启动应用程序池?
You could register a route that contains the SEO slug part of your URL, and then use the slug in a lookup.
您可以注册包含URL的SEO slug部分的路由,然后在查找中使用slug。
RouteConfig.cs
routes.MapRoute(
name: "SeoSlugPageLookup",
url: "Page/{slug}",
defaults: new { controller = "Page",
action = "SlugLookup",
});
PageController.cs
public ActionResult SlugLookup (string slug)
{
// TODO: Check for null/empty slug here.
int? id = GetPageId (slug);
if (id != null) {
return View ("Show", new { id });
}
// TODO: The fallback should help the user by searching your site for the slug.
throw new HttpException (404, "NotFound");
}
private int? GetPageId (string slug)
{
int? id = GetPageIdFromCache (slug);
if (id == null) {
id = GetPageIdFromDatabase (slug);
if (id != null) {
SetPageIdInCache (slug, id);
}
}
return id;
}
private int? GetPageIdFromCache (string slug)
{
// There are many caching techniques for example:
// http://msdn.microsoft.com/en-us/library/dd287191.aspx
// http://alandjackson.wordpress.com/2012/04/17/key-based-cache-in-mvc3-5/
// Depending on how advanced you want your CMS to be,
// caching could be done in a service layer.
return slugToPageIdCache.ContainsKey (slug) ? slugToPageIdCache [slug] : null;
}
private int? SetPageIdInCache (string slug, int id)
{
return slugToPageIdCache.GetOrAdd (slug, id);
}
private int? GetPageIdFromDatabase (string slug)
{
using (CMSContext db = new CMSContext()) {
// Assumes unique slugs.
Page page = db.Pages.Where (p => p.Slug == requestContext.Url).SingleOrDefault ();
if (page != null) {
return page.Id;
}
}
return null;
}
public ActionResult Show (int id)
{
// Your existing implementation.
}
(FYI: Code not compiled nor tested - haven't got my dev environment available right now. Treat it as pseudocode ;)
(仅供参考:代码未编译也未经过测试 - 目前还没有我的开发环境可用。将其视为伪代码;)
This implementation will have one search for the slug per server restart. You could also pre-populate the key-value slug-to-id cache at startup, so all existing page lookups will be cheap.
这个实现将搜索每个服务器重启的slug。您还可以在启动时预先填充键值slug-to-id缓存,这样所有现有的页面查找都会很便宜。
#2
0
I've edited my answer to give a more complete answer to your questions:
我编辑了我的答案,以便为您的问题提供更完整的答案:
Answer to Question 1:
对问题1的回答:
Registering routes is initialized on startup. (Perhaps also when the Application Pool
recycles, it's highly probable.) I also think there is nothing wrong with your approach since it is occuring only once. I do the same thing querying all supported languages from the database to register them as /TwoLetterISOLanguageName (/nl, /en, /de, etc.).
注册路由在启动时初始化。 (也许当应用程序池回收时,它很有可能。)我也认为你的方法没有任何问题,因为它只发生一次。我做同样的事情从数据库查询所有支持的语言,将它们注册为/ TwoLetterISOLanguageName(/ nl,/ en,/ de等)。
Answer to Question 2:
对问题2的回答:
This should work passing a model: Put it before the Default
route!
这应该可以传递一个模型:把它放在默认路由之前!
routes.MapRoute(
name: "Contact",
url: "contact/{action}",
defaults: new { controller = "Contact",
action = "Index",
MyModel = new MyModel { Name = "hello" } });
The ContactController:
public ActionResult Index(MyModel mymodel)
{
return Content(mymodel.Name);
}
The Model:
public class MyModel
{
public string Name { get; set; }
}
#1
2
Even if your route registration code works as is, the problem will be that the routes are registered statically only on startup. What happens when a new post is added - would you have to restart the app pool?
即使您的路由注册代码按原样工作,问题仍然是路由仅在启动时静态注册。添加新帖子后会发生什么 - 您是否需要重新启动应用程序池?
You could register a route that contains the SEO slug part of your URL, and then use the slug in a lookup.
您可以注册包含URL的SEO slug部分的路由,然后在查找中使用slug。
RouteConfig.cs
routes.MapRoute(
name: "SeoSlugPageLookup",
url: "Page/{slug}",
defaults: new { controller = "Page",
action = "SlugLookup",
});
PageController.cs
public ActionResult SlugLookup (string slug)
{
// TODO: Check for null/empty slug here.
int? id = GetPageId (slug);
if (id != null) {
return View ("Show", new { id });
}
// TODO: The fallback should help the user by searching your site for the slug.
throw new HttpException (404, "NotFound");
}
private int? GetPageId (string slug)
{
int? id = GetPageIdFromCache (slug);
if (id == null) {
id = GetPageIdFromDatabase (slug);
if (id != null) {
SetPageIdInCache (slug, id);
}
}
return id;
}
private int? GetPageIdFromCache (string slug)
{
// There are many caching techniques for example:
// http://msdn.microsoft.com/en-us/library/dd287191.aspx
// http://alandjackson.wordpress.com/2012/04/17/key-based-cache-in-mvc3-5/
// Depending on how advanced you want your CMS to be,
// caching could be done in a service layer.
return slugToPageIdCache.ContainsKey (slug) ? slugToPageIdCache [slug] : null;
}
private int? SetPageIdInCache (string slug, int id)
{
return slugToPageIdCache.GetOrAdd (slug, id);
}
private int? GetPageIdFromDatabase (string slug)
{
using (CMSContext db = new CMSContext()) {
// Assumes unique slugs.
Page page = db.Pages.Where (p => p.Slug == requestContext.Url).SingleOrDefault ();
if (page != null) {
return page.Id;
}
}
return null;
}
public ActionResult Show (int id)
{
// Your existing implementation.
}
(FYI: Code not compiled nor tested - haven't got my dev environment available right now. Treat it as pseudocode ;)
(仅供参考:代码未编译也未经过测试 - 目前还没有我的开发环境可用。将其视为伪代码;)
This implementation will have one search for the slug per server restart. You could also pre-populate the key-value slug-to-id cache at startup, so all existing page lookups will be cheap.
这个实现将搜索每个服务器重启的slug。您还可以在启动时预先填充键值slug-to-id缓存,这样所有现有的页面查找都会很便宜。
#2
0
I've edited my answer to give a more complete answer to your questions:
我编辑了我的答案,以便为您的问题提供更完整的答案:
Answer to Question 1:
对问题1的回答:
Registering routes is initialized on startup. (Perhaps also when the Application Pool
recycles, it's highly probable.) I also think there is nothing wrong with your approach since it is occuring only once. I do the same thing querying all supported languages from the database to register them as /TwoLetterISOLanguageName (/nl, /en, /de, etc.).
注册路由在启动时初始化。 (也许当应用程序池回收时,它很有可能。)我也认为你的方法没有任何问题,因为它只发生一次。我做同样的事情从数据库查询所有支持的语言,将它们注册为/ TwoLetterISOLanguageName(/ nl,/ en,/ de等)。
Answer to Question 2:
对问题2的回答:
This should work passing a model: Put it before the Default
route!
这应该可以传递一个模型:把它放在默认路由之前!
routes.MapRoute(
name: "Contact",
url: "contact/{action}",
defaults: new { controller = "Contact",
action = "Index",
MyModel = new MyModel { Name = "hello" } });
The ContactController:
public ActionResult Index(MyModel mymodel)
{
return Content(mymodel.Name);
}
The Model:
public class MyModel
{
public string Name { get; set; }
}