You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vapor 3中SQLiteModel对象的Slug生成与Leaf使用问题

在Vapor 3中为SQLiteModel生成Slug并在Leaf模板中使用

咱们一步步来搞定这个搜索引擎友好链接的问题,从slug生成到Leaf模板使用全给你捋清楚:

1. 给模型添加Slug属性与自动生成逻辑

首先得在你的SQLiteModel(比如Tag)里新增slug字段,还要写个能生成唯一slug的方法——毕竟不能出现两个相同的slug对吧?

import Vapor
import FluentSQLite

final class Tag: SQLiteModel {
    var id: Int?
    var name: String
    var slug: String // 新增的slug字段
    
    init(id: Int? = nil, name: String, slug: String) {
        self.id = id
        self.name = name
        self.slug = slug
    }
}

// 扩展模型,实现slug生成逻辑
extension Tag {
    // 把名称转成基础slug:小写、替换特殊字符为横杠
    private static func baseSlug(from name: String) -> String {
        return name.lowercased()
            .replacingOccurrences(of: "[^a-z0-9]+", with: "-", options: .regularExpression)
            .trimmingCharacters(in: .punctuationCharacters)
    }
    
    // 生成唯一slug:如果基础slug已存在,就加数字后缀
    static func generateUniqueSlug(for name: String, on conn: DatabaseConnectable) throws -> Future<String> {
        let baseSlug = self.baseSlug(from: name)
        
        return Tag.query(on: conn)
            .filter(\.slug == baseSlug)
            .count()
            .map { count in
                count == 0 ? baseSlug : "\(baseSlug)-\(count)"
            }
    }
}

// 迁移时给slug加唯一约束
extension Tag: Migration {
    static func prepare(on conn: SQLiteConnection) -> Future<Void> {
        return SQLiteDatabase.create(Tag.self, on: conn) { builder in
            builder.field(for: \.id, isIdentifier: true)
            builder.field(for: \.name)
            builder.field(for: \.slug)
            builder.unique(on: \.slug) // 确保slug唯一
        }
    }
}

创建Tag的时候别手动传slug,在控制器里调用生成方法:

// 接收创建请求的结构体
struct TagCreateData: Content {
    var name: String
}

func createTag(_ req: Request) throws -> Future<Tag> {
    return try req.content.decode(TagCreateData.self).flatMap { data in
        // 先生成唯一slug,再创建Tag并保存
        return Tag.generateUniqueSlug(for: data.name, on: req)
            .map { Tag(name: data.name, slug: $0) }
            .flatMap { $0.save(on: req) }
    }
}

2. 实现Parameter协议,让Vapor通过Slug查找模型

接下来要让Vapor支持用slug代替id来查询模型,给Tag扩展Parameter协议:

extension Tag: Parameter {
    // 指定用slug作为参数匹配的键
    static var parameterKey: String {
        return "slug"
    }
    
    // 实现通过slug查找模型的逻辑
    static func parameter(for value: String, on conn: DatabaseConnectable) throws -> Future<Tag> {
        return Tag.query(on: conn)
            .filter(\.slug == value)
            .first()
            .unwrap(or: Abort(.notFound, reason: "找不到slug为\(value)的标签"))
    }
}

然后更新路由,不用改路径写法,Vapor会自动用slug匹配:

router.get("tag", Tag.parameter, use: tagDetailHandler)

// 详情页控制器方法
func tagDetailHandler(_ req: Request) throws -> Future<View> {
    let tag = try req.parameters.next(Tag.self)
    return try req.view().render("tag/detail", tag)
}

3. 修正Leaf模板中的链接

你之前的模板用的是tag.id,现在直接改成tag.slug就行,确保变量名和模型属性完全一致(大小写敏感哦):

#for(tag in tags) {
    <a class="tag" href="/tag/#(tag.slug)">#(tag.name)</a>
}

关于你说的#(model.uniqueSlug)无效的问题

你提到加了uniqueSlug属性但Leaf里用不了,大概率是这几个原因:

  • 如果uniqueSlug是计算属性,要确保它是var且有正确的getter,比如:
    extension Tag {
        var uniqueSlug: String {
            return slug // 或者你的自定义逻辑
        }
    }
    
  • 检查渲染模板时有没有把完整的模型实例传进去,别只传了部分属性
  • 确认模板里的变量名和模型属性名完全匹配(比如别把uniqueSlug写成uniqueslug)

4. 测试验证

现在创建标签时,系统会自动生成唯一slug:比如第一个"Swift"标签的slug是swift,再创建同名标签就会生成swift-1,访问/tag/swift就能直接跳转到对应详情页,完全符合搜索引擎友好的要求啦。

内容的提问来源于stack exchange,提问作者Austin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 03:39:58