URL路由模式技能Skill url-routing-patterns

此技能用于设计和实现URL路由模式,包括URL结构设计、slug生成、重定向管理、本地化URL支持以及路由API构建,适用于无头CMS架构,有助于SEO优化和用户体验。关键词:URL路由、slug生成、SEO、重定向、本地化、无头CMS、路由API。

后端开发 0 次安装 0 次浏览 更新于 3/11/2026

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端点