name: url-routing-patterns description: 用于设计URL结构、slug生成、SEO友好的URL、重定向或本地化URL模式时使用。涵盖路由配置、URL重写、规范URL和无头CMS的路由API。 allowed-tools: Read, Glob, Grep, Task, Skill
URL路由模式
为无头CMS架构设计URL结构、slug生成和路由策略的指南。
何时使用此技能
- 设计SEO友好的URL结构
- 实现slug生成
- 配置重定向管理
- 规划本地化URL模式
- 构建路由API
URL结构模式
分层URL(基于页面)
/ # 首页
/about # 关于页面
/about/team # 团队(关于的子页面)
/about/team/leadership # 领导层(孙页面)
/products # 产品列表
/products/software # 软件类别
/products/software/crm # 特定产品
内容类型URL(基于集合)
/blog # 博客列表
/blog/2025/01/my-article # 带日期的博客文章
/blog/my-article # 不带日期的博客文章
/docs # 文档首页
/docs/getting-started # 文档部分
/docs/getting-started/install # 文档页面
/team/jane-doe # 团队成员档案
/portfolio/project-alpha # 作品集项目
混合URL
/products/software/crm # 类别 > 产品
/blog/technology/ai-trends # 类别 > 文章
/help/faq/billing # 部分 > 主题
Slug生成
Slug服务
public class SlugService
{
public string GenerateSlug(string text, SlugOptions? options = null)
{
options ??= new SlugOptions();
var slug = text
.ToLowerInvariant()
.Normalize(NormalizationForm.FormD);
// 去除变音符号
slug = new string(slug
.Where(c => CharUnicodeInfo.GetUnicodeCategory(c)
!= UnicodeCategory.NonSpacingMark)
.ToArray());
// 替换空格和无效字符
slug = Regex.Replace(slug, @"[^a-z0-9\s-]", "");
slug = Regex.Replace(slug, @"\s+", "-");
slug = Regex.Replace(slug, @"-+", "-");
slug = slug.Trim('-');
// 强制最大长度
if (slug.Length > options.MaxLength)
{
slug = slug.Substring(0, options.MaxLength).TrimEnd('-');
}
return slug;
}
public async Task<string> GenerateUniqueSlugAsync(
string text,
string contentType,
Guid? excludeId = null)
{
var baseSlug = GenerateSlug(text);
var slug = baseSlug;
var counter = 1;
while (await SlugExistsAsync(slug, contentType, excludeId))
{
slug = $"{baseSlug}-{counter}";
counter++;
}
return slug;
}
}
public class SlugOptions
{
public int MaxLength { get; set; } = 100;
public bool AllowUnicode { get; set; } = false;
public string Separator { get; set; } = "-";
}
自动路由模式
public class AutorouteSettings
{
public string Pattern { get; set; } = string.Empty;
public bool AllowCustom { get; set; } = true;
public bool ShowHomepageOption { get; set; }
}
// 模式示例:
// "{ContentType}/{Slug}" -> /article/my-title
// "{Category.Slug}/{Slug}" -> /technology/my-article
// "blog/{CreatedUtc.Year}/{Slug}" -> /blog/2025/my-article
// "{Parent.Path}/{Slug}" -> /about/team/leadership
public class AutorouteService
{
public string GeneratePath(ContentItem item, string pattern)
{
var path = pattern;
// 替换令牌
path = path.Replace("{Slug}", item.Slug);
path = path.Replace("{ContentType}", item.ContentType.ToLower());
path = path.Replace("{CreatedUtc.Year}", item.CreatedUtc.Year.ToString());
path = path.Replace("{CreatedUtc.Month}",
item.CreatedUtc.Month.ToString("00"));
// 处理关系
if (path.Contains("{Category.Slug}") && item.CategoryId.HasValue)
{
var category = _categoryRepository.Get(item.CategoryId.Value);
path = path.Replace("{Category.Slug}", category?.Slug ?? "uncategorized");
}
// 处理父路径
if (path.Contains("{Parent.Path}") && item.ParentId.HasValue)
{
var parent = _contentRepository.Get(item.ParentId.Value);
path = path.Replace("{Parent.Path}", parent?.Path ?? "");
}
// 规范化路径
path = "/" + path.Trim('/').ToLowerInvariant();
return path;
}
}
重定向管理
重定向类型
public class Redirect
{
public Guid Id { get; set; }
public string FromPath { get; set; } = string.Empty;
public string ToPath { get; set; } = string.Empty;
public RedirectType Type { get; set; }
public bool IsRegex { get; set; }
public bool PreserveQueryString { get; set; }
public DateTime? ExpiresUtc { get; set; }
}
public enum RedirectType
{
Permanent = 301, // 永久移动(SEO传递)
Temporary = 302, // 找到(临时重定向)
SeeOther = 303, // 查看其他(POST到GET)
TemporaryRedirect = 307, // 临时(保留方法)
PermanentRedirect = 308 // 永久(保留方法)
}
Slug更改时的自动重定向
public class ContentUpdateHandler
{
public async Task HandleSlugChangeAsync(
Guid contentId,
string oldPath,
string newPath)
{
if (oldPath == newPath) return;
// 创建从旧到新的重定向
var redirect = new Redirect
{
Id = Guid.NewGuid(),
FromPath = oldPath,
ToPath = newPath,
Type = RedirectType.Permanent,
PreserveQueryString = true
};
await _redirectRepository.AddAsync(redirect);
// 更新指向旧路径的任何现有重定向
var existingRedirects = await _redirectRepository
.GetByToPathAsync(oldPath);
foreach (var existing in existingRedirects)
{
existing.ToPath = newPath;
await _redirectRepository.UpdateAsync(existing);
}
}
}
本地化URL
URL本地化策略
| 策略 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| 路径前缀 | /en/about, /fr/about |
清晰,SEO友好 | URL较长 |
| 子域名 | en.site.com, fr.site.com |
独立托管 | 设置复杂 |
| 查询参数 | /about?lang=fr |
简单 | SEO差 |
| 翻译slug | /about, /a-propos |
自然 | 难以管理 |
路径前缀实现
public class LocalizedRoutingService
{
private readonly string[] _supportedLocales = { "en", "fr", "de", "es" };
private readonly string _defaultLocale = "en";
public string GetLocalizedPath(string path, string locale)
{
// 移除现有语言前缀
var cleanPath = RemoveLocalePrefix(path);
// 添加新语言前缀(默认语言跳过)
if (locale != _defaultLocale)
{
return $"/{locale}{cleanPath}";
}
return cleanPath;
}
public (string path, string locale) ParseLocalizedPath(string requestPath)
{
foreach (var locale in _supportedLocales)
{
if (requestPath.StartsWith($"/{locale}/") ||
requestPath == $"/{locale}")
{
var path = requestPath.Substring(locale.Length + 1);
return (string.IsNullOrEmpty(path) ? "/" : path, locale);
}
}
return (requestPath, _defaultLocale);
}
}
Hreflang标签
public class HreflangService
{
public List<HreflangTag> GenerateHreflangTags(
ContentItem content,
string baseUrl)
{
var tags = new List<HreflangTag>();
// 获取所有本地化版本
var localizations = _localizationService
.GetLocalizedVersions(content.Id);
foreach (var loc in localizations)
{
tags.Add(new HreflangTag
{
Hreflang = loc.Locale,
Href = $"{baseUrl}{GetLocalizedPath(content.Path, loc.Locale)}"
});
}
// 添加x-default
tags.Add(new HreflangTag
{
Hreflang = "x-default",
Href = $"{baseUrl}{content.Path}"
});
return tags;
}
}
public class HreflangTag
{
public string Hreflang { get; set; } = string.Empty;
public string Href { get; set; } = string.Empty;
}
规范URL
public class CanonicalUrlService
{
public string GetCanonicalUrl(HttpRequest request, ContentItem content)
{
var baseUrl = $"{request.Scheme}://{request.Host}";
// 使用内容的主要路径作为规范
var canonicalPath = content.PrimaryPath ?? content.Path;
// 移除查询参数(除非分页)
// 规范化尾部斜杠
return $"{baseUrl}{canonicalPath}";
}
}
URL规范化
public class UrlNormalizer
{
public string Normalize(string url, NormalizationOptions options)
{
var uri = new UriBuilder(url);
// 小写路径
uri.Path = uri.Path.ToLowerInvariant();
// 处理尾部斜杠
if (options.TrailingSlash == TrailingSlashBehavior.Remove)
{
uri.Path = uri.Path.TrimEnd('/');
}
else if (options.TrailingSlash == TrailingSlashBehavior.Add &&
!uri.Path.EndsWith('/'))
{
uri.Path += '/';
}
// 排序查询参数
if (options.SortQueryParams && !string.IsNullOrEmpty(uri.Query))
{
var queryParams = HttpUtility.ParseQueryString(uri.Query);
var sorted = queryParams.AllKeys
.OrderBy(k => k)
.Select(k => $"{k}={queryParams[k]}");
uri.Query = string.Join("&", sorted);
}
return uri.ToString();
}
}
public class NormalizationOptions
{
public TrailingSlashBehavior TrailingSlash { get; set; }
public bool SortQueryParams { get; set; }
public bool ForceLowercase { get; set; } = true;
}
public enum TrailingSlashBehavior
{
Remove,
Add,
Preserve
}
路由API
端点
GET /api/routes/resolve?path=/about/team # 将路径解析为内容
GET /api/redirects # 列出重定向
GET /api/sitemap.xml # XML网站地图
POST /api/slugs/generate # 从文本生成slug
POST /api/slugs/validate # 检查slug可用性
路由解析响应
{
"data": {
"path": "/about/team",
"contentId": "page-456",
"contentType": "Page",
"locale": "en",
"canonical": "https://example.com/about/team",
"alternates": [
{ "hreflang": "fr", "href": "https://example.com/fr/a-propos/equipe" },
{ "hreflang": "de", "href": "https://example.com/de/uber-uns/team" }
],
"breadcrumbs": [
{ "label": "首页", "path": "/" },
{ "label": "关于", "path": "/about" },
{ "label": "团队", "path": "/about/team" }
]
}
}
相关技能
page-structure-design- 用于URL的页面层次结构navigation-architecture- 菜单链接和路径headless-api-design- 路由API端点