url-routing-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseURL Routing Patterns
URL路由模式
Guidance for designing URL structures, slug generation, and routing strategies for headless CMS architectures.
为无头CMS架构提供URL结构设计、slug生成及路由策略的指导方案。
When to Use This Skill
适用场景
- Designing SEO-friendly URL structures
- Implementing slug generation
- Configuring redirect management
- Planning localized URL patterns
- Building routing APIs
- 设计SEO友好型URL结构
- 实现slug生成功能
- 配置重定向管理
- 规划本地化URL模式
- 构建路由API
URL Structure Patterns
URL结构模式
Hierarchical URLs (Page-Based)
层级式URL(基于页面)
text
/ # Home
/about # About page
/about/team # Team (child of About)
/about/team/leadership # Leadership (grandchild)
/products # Products listing
/products/software # Software category
/products/software/crm # Specific producttext
/ # 首页
/about # 关于页面
/about/team # 团队页面(关于页面的子页面)
/about/team/leadership # 领导层页面(团队页面的子页面)
/products # 产品列表页
/products/software # 软件分类页
/products/software/crm # 具体产品页Content Type URLs (Collection-Based)
内容类型URL(基于集合)
text
/blog # Blog listing
/blog/2025/01/my-article # Blog post with date
/blog/my-article # Blog post without date
/docs # Documentation home
/docs/getting-started # Doc section
/docs/getting-started/install # Doc page
/team/jane-doe # Team member profile
/portfolio/project-alpha # Portfolio itemtext
/blog # 博客列表页
/blog/2025/01/my-article # 带日期的博客文章
/blog/my-article # 不带日期的博客文章
/docs # 文档首页
/docs/getting-started # 文档章节
/docs/getting-started/install # 文档页面
/team/jane-doe # 团队成员简介
/portfolio/project-alpha # 作品集项目Hybrid URLs
混合式URL
text
/products/software/crm # Category > Product
/blog/technology/ai-trends # Category > Post
/help/faq/billing # Section > Topictext
/products/software/crm # 分类 > 产品
/blog/technology/ai-trends # 分类 > 文章
/help/faq/billing # 章节 > 主题Slug Generation
Slug生成
Slug Service
Slug服务
csharp
public class SlugService
{
public string GenerateSlug(string text, SlugOptions? options = null)
{
options ??= new SlugOptions();
var slug = text
.ToLowerInvariant()
.Normalize(NormalizationForm.FormD);
// Remove diacritics
slug = new string(slug
.Where(c => CharUnicodeInfo.GetUnicodeCategory(c)
!= UnicodeCategory.NonSpacingMark)
.ToArray());
// Replace spaces and invalid chars
slug = Regex.Replace(slug, @"[^a-z0-9\s-]", "");
slug = Regex.Replace(slug, @"\s+", "-");
slug = Regex.Replace(slug, @"-+", "-");
slug = slug.Trim('-');
// Enforce max length
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; } = "-";
}csharp
public class SlugService
{
public string GenerateSlug(string text, SlugOptions? options = null)
{
options ??= new SlugOptions();
var slug = text
.ToLowerInvariant()
.Normalize(NormalizationForm.FormD);
// Remove diacritics
slug = new string(slug
.Where(c => CharUnicodeInfo.GetUnicodeCategory(c)
!= UnicodeCategory.NonSpacingMark)
.ToArray());
// Replace spaces and invalid chars
slug = Regex.Replace(slug, @"[^a-z0-9\s-]", "");
slug = Regex.Replace(slug, @"\s+", "-");
slug = Regex.Replace(slug, @"-+", "-");
slug = slug.Trim('-');
// Enforce max length
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; } = "-";
}Autoroute Patterns
自动路由模式
csharp
public class AutorouteSettings
{
public string Pattern { get; set; } = string.Empty;
public bool AllowCustom { get; set; } = true;
public bool ShowHomepageOption { get; set; }
}
// Pattern examples:
// "{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;
// Replace tokens
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"));
// Handle relationships
if (path.Contains("{Category.Slug}") && item.CategoryId.HasValue)
{
var category = _categoryRepository.Get(item.CategoryId.Value);
path = path.Replace("{Category.Slug}", category?.Slug ?? "uncategorized");
}
// Handle parent path
if (path.Contains("{Parent.Path}") && item.ParentId.HasValue)
{
var parent = _contentRepository.Get(item.ParentId.Value);
path = path.Replace("{Parent.Path}", parent?.Path ?? "");
}
// Normalize path
path = "/" + path.Trim('/').ToLowerInvariant();
return path;
}
}csharp
public class AutorouteSettings
{
public string Pattern { get; set; } = string.Empty;
public bool AllowCustom { get; set; } = true;
public bool ShowHomepageOption { get; set; }
}
// Pattern examples:
// "{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;
// Replace tokens
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"));
// Handle relationships
if (path.Contains("{Category.Slug}") && item.CategoryId.HasValue)
{
var category = _categoryRepository.Get(item.CategoryId.Value);
path = path.Replace("{Category.Slug}", category?.Slug ?? "uncategorized");
}
// Handle parent path
if (path.Contains("{Parent.Path}") && item.ParentId.HasValue)
{
var parent = _contentRepository.Get(item.ParentId.Value);
path = path.Replace("{Parent.Path}", parent?.Path ?? "");
}
// Normalize path
path = "/" + path.Trim('/').ToLowerInvariant();
return path;
}
}Redirect Management
重定向管理
Redirect Types
重定向类型
csharp
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, // Moved permanently (SEO transfers)
Temporary = 302, // Found (temporary redirect)
SeeOther = 303, // See other (POST to GET)
TemporaryRedirect = 307, // Temporary (preserves method)
PermanentRedirect = 308 // Permanent (preserves method)
}csharp
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, // Moved permanently (SEO transfers)
Temporary = 302, // Found (temporary redirect)
SeeOther = 303, // See other (POST to GET)
TemporaryRedirect = 307, // Temporary (preserves method)
PermanentRedirect = 308 // Permanent (preserves method)
}Automatic Redirect on Slug Change
Slug变更时自动创建重定向
csharp
public class ContentUpdateHandler
{
public async Task HandleSlugChangeAsync(
Guid contentId,
string oldPath,
string newPath)
{
if (oldPath == newPath) return;
// Create redirect from old to new
var redirect = new Redirect
{
Id = Guid.NewGuid(),
FromPath = oldPath,
ToPath = newPath,
Type = RedirectType.Permanent,
PreserveQueryString = true
};
await _redirectRepository.AddAsync(redirect);
// Update any existing redirects pointing to old path
var existingRedirects = await _redirectRepository
.GetByToPathAsync(oldPath);
foreach (var existing in existingRedirects)
{
existing.ToPath = newPath;
await _redirectRepository.UpdateAsync(existing);
}
}
}csharp
public class ContentUpdateHandler
{
public async Task HandleSlugChangeAsync(
Guid contentId,
string oldPath,
string newPath)
{
if (oldPath == newPath) return;
// Create redirect from old to new
var redirect = new Redirect
{
Id = Guid.NewGuid(),
FromPath = oldPath,
ToPath = newPath,
Type = RedirectType.Permanent,
PreserveQueryString = true
};
await _redirectRepository.AddAsync(redirect);
// Update any existing redirects pointing to old path
var existingRedirects = await _redirectRepository
.GetByToPathAsync(oldPath);
foreach (var existing in existingRedirects)
{
existing.ToPath = newPath;
await _redirectRepository.UpdateAsync(existing);
}
}
}Localized URLs
本地化URL
URL Localization Strategies
URL本地化策略
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| Path prefix | | Clear, SEO-friendly | Longer URLs |
| Subdomain | | Separate hosting | Complex setup |
| Query param | | Simple | Poor SEO |
| Translated slugs | | Natural | Hard to manage |
| 策略 | 示例 | 优势 | 劣势 |
|---|---|---|---|
| 路径前缀 | | 清晰、SEO友好 | URL长度更长 |
| 子域名 | | 可独立托管 | 配置复杂 |
| 查询参数 | | 实现简单 | SEO表现差 |
| 翻译Slug | | 符合自然语言习惯 | 管理难度大 |
Path Prefix Implementation
路径前缀实现
csharp
public class LocalizedRoutingService
{
private readonly string[] _supportedLocales = { "en", "fr", "de", "es" };
private readonly string _defaultLocale = "en";
public string GetLocalizedPath(string path, string locale)
{
// Remove existing locale prefix
var cleanPath = RemoveLocalePrefix(path);
// Add new locale prefix (skip for default)
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);
}
}csharp
public class LocalizedRoutingService
{
private readonly string[] _supportedLocales = { "en", "fr", "de", "es" };
private readonly string _defaultLocale = "en";
public string GetLocalizedPath(string path, string locale)
{
// Remove existing locale prefix
var cleanPath = RemoveLocalePrefix(path);
// Add new locale prefix (skip for default)
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 Tags
Hreflang标签
csharp
public class HreflangService
{
public List<HreflangTag> GenerateHreflangTags(
ContentItem content,
string baseUrl)
{
var tags = new List<HreflangTag>();
// Get all localized versions
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)}"
});
}
// Add 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;
}csharp
public class HreflangService
{
public List<HreflangTag> GenerateHreflangTags(
ContentItem content,
string baseUrl)
{
var tags = new List<HreflangTag>();
// Get all localized versions
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)}"
});
}
// Add 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;
}Canonical URLs
规范URL
csharp
public class CanonicalUrlService
{
public string GetCanonicalUrl(HttpRequest request, ContentItem content)
{
var baseUrl = $"{request.Scheme}://{request.Host}";
// Use content's primary path as canonical
var canonicalPath = content.PrimaryPath ?? content.Path;
// Remove query parameters (unless paginated)
// Normalize trailing slash
return $"{baseUrl}{canonicalPath}";
}
}csharp
public class CanonicalUrlService
{
public string GetCanonicalUrl(HttpRequest request, ContentItem content)
{
var baseUrl = $"{request.Scheme}://{request.Host}";
// Use content's primary path as canonical
var canonicalPath = content.PrimaryPath ?? content.Path;
// Remove query parameters (unless paginated)
// Normalize trailing slash
return $"{baseUrl}{canonicalPath}";
}
}URL Normalization
URL标准化
csharp
public class UrlNormalizer
{
public string Normalize(string url, NormalizationOptions options)
{
var uri = new UriBuilder(url);
// Lowercase path
uri.Path = uri.Path.ToLowerInvariant();
// Handle trailing slash
if (options.TrailingSlash == TrailingSlashBehavior.Remove)
{
uri.Path = uri.Path.TrimEnd('/');
}
else if (options.TrailingSlash == TrailingSlashBehavior.Add &&
!uri.Path.EndsWith('/'))
{
uri.Path += '/';
}
// Sort query parameters
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
}csharp
public class UrlNormalizer
{
public string Normalize(string url, NormalizationOptions options)
{
var uri = new UriBuilder(url);
// Lowercase path
uri.Path = uri.Path.ToLowerInvariant();
// Handle trailing slash
if (options.TrailingSlash == TrailingSlashBehavior.Remove)
{
uri.Path = uri.Path.TrimEnd('/');
}
else if (options.TrailingSlash == TrailingSlashBehavior.Add &&
!uri.Path.EndsWith('/'))
{
uri.Path += '/';
}
// Sort query parameters
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
}Routing API
路由API
Endpoints
接口端点
text
GET /api/routes/resolve?path=/about/team # Resolve path to content
GET /api/redirects # List redirects
GET /api/sitemap.xml # XML sitemap
POST /api/slugs/generate # Generate slug from text
POST /api/slugs/validate # Check slug availabilitytext
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是否可用Route Resolution Response
路由解析响应
json
{
"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": "Home", "path": "/" },
{ "label": "About", "path": "/about" },
{ "label": "Team", "path": "/about/team" }
]
}
}json
{
"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": "Home", "path": "/" },
{ "label": "About", "path": "/about" },
{ "label": "Team", "path": "/about/team" }
]
}
}Related Skills
相关技能
- - Page hierarchy for URLs
page-structure-design - - Menu links and paths
navigation-architecture - - Routing API endpoints
headless-api-design
- - 用于URL的页面层级设计
page-structure-design - - 菜单链接与路径设计
navigation-architecture - - 路由API端点设计
headless-api-design