主题开发:渲染变量与数据约束(ViewModel 约定)
解释模板渲染变量、字段约束与兼容策略:缺字段/空列表/分页等。
ViewModel(渲染变量)与数据约束(主题开发约定)
本文从 aithemes.md 提炼:主题模板(Go template)可用的渲染变量(usecase.ViewModel)以及与之相关的数据形态/约束,供主题开发时直接对照使用。
1. 顶层渲染对象:.(usecase.ViewModel)
所有页面渲染时注入的顶层数据结构是 usecase.ViewModel,模板中用 . 访问。
1.1 字段清单(模板可用)
- Uri当前请求 uri
- Site(*conf.SiteOption)站点全局配置
- ThemesExt(map[string]any)主题扩展数据(来自主题的 vvdata.json)
- Type(string)页面类型常见:home、category、page、article、search、tag、user、product、not_found 等
- Title / Description / Keywords页面 TDK(有兜底)
- List([]*ContentCardVM)列表数据(首页/分类/搜索/标签/用户等)
- Pager(PagerVM)分页数据
- Columns([]*models.Column)栏目列表(常用于导航 nav)
- Nav([]*models.Column)面包屑栏目链
- Single(*ColumnVM)单页数据(也可在分类/详情页作为“当前栏目完整信息”使用,见下)
- Content(*ContentCardVM)文章/内容详情
- CurrentColumn(*models.Column)当前栏目信息(导航高亮、上下文)
- Request(*RequestData)请求数据(例如模板可读 Request.Query.key)
- Prev / Next(*ContentCardVM)上一篇/下一篇(仅详情页可能存在)
1.2 通用约束(必须遵守)
- 模板里不要假设字段必定非空:Site / Single / Content / Prev / Next / Request 都可能为空使用前先 {{if ...}} 做空判断,避免 nil 访问
- 字段名大小写严格按 struct:ContentCardVM 的链接字段是 URL(全大写),不是 .Url
- 访问 map 推荐写法:{{ index .ThemesExt "key" }}{{ index .Content.Ext "key" }}不存在 key 会得到 <no value>,建议配合 if 判断
2. 页面类型(.Type)与数据形态
.Type 用来区分页面数据形态,主题模板务必按它做分支,避免在错误页面读取错误字段。
2.1 首页(Home)
- 触发:Uri == "/"
- 通常:.Type == "home".List + .Pager 有意义轮播/推荐等组件数据通常通过模板函数读取(与 .List 无关)
2.2 分类页(Category:承载列表数据)
- 触发:/{colSlug} 且栏目不是跳转/外链/单页模式
- 通常:.Type == "category".CurrentColumn 存当前栏目(用于导航高亮).List + .Pager 为主要列表数据.Single 可能存在,用作栏目完整信息(栏目 banner/SEO/扩展等)
2.3 详情页(Detail:文章/产品等内容详情)
- 触发:/{colSlug}/{postSlug}
- 通常:.Type == "article".Content 为详情内容.Prev / .Next 可能存在
2.4 单页(Page:栏目自身内容页)
- 触发:/{colSlug} 且栏目渲染模式为单页(如 RenderMode=2)
- 通常:.Type == "page".Single 为单页栏目数据正文通常读:{{.Single.Content|Str2Html}}
2.5 搜索/标签/用户列表等(复用列表形态)
- 通常复用 category.html 的列表结构:.Type 可能为 search / tag / user可能没有 .Single(因为不是栏目页),模板需要 {{if .Single}}...{{end}} 兼容
3. 主题扩展数据:ThemesExt(vvdata.json)
- 来源:app1/themes/<theme_name>/vvdata.json
- 注入:模板 ViewModel 的 ThemesExt(map[string]any)
3.1 数据约束
- vvdata.json 必须是合法 JSON 对象({}),否则会被当作空对象处理
3.2 模板访问方式
- 整体:{{ .ThemesExt }}
- 取 key:{{ index .ThemesExt "key" }}
4. 内容与栏目扩展数据(Ext):约束与用法
4.1 ContentCardVM.Ext(内容扩展值)
- ContentCardVM.Ext:内容的扩展数据(map)
模板读取示例:
- 详情页:
{{ $v := index .Content.Ext "age" }} {{ if $v }}<p class="sub">{{$v}}</p>{{ end }}
- 列表页:
{{ range .List }} {{ $v := index .Ext "age" }} {{ if $v }}<span>{{$v}}</span>{{ end }} {{ end }}
4.2 ColumnVM.Ext(栏目扩展)
- ColumnVM.Ext:栏目 ext 的 map(模板侧扩展点)
- 在模板侧常见访问:{{ index .Single.Ext "xxx" }}
语义约束:
- 单页(.Type == "page")时:.Single.Ext 的语义是 kv(键值对),用于主题展示定制(开关/颜色/布局/SEO 辅助字段等)
- 文章/商品等栏目:栏目 ext 也可以承载“表单 schema”(见 4.3)
4.3 栏目 ext 作为“表单元数据(schema)”的约定
适用场景:栏目类型是“文章/商品”等需要自定义字段的内容模型时,把字段定义放在栏目 ext 里。
- 栏目 ext:存字段定义(schema)
- 内容 ext:存字段值(value)
推荐 Column.ext JSON 结构:
{ "fields": [ { "defaultValue": "", "key": "age", "label": "abc", "options": "", "placeholder": "", "required": false, "type": "input" }, { "defaultValue": "", "key": "name", "label": "asdg", "options": "", "placeholder": "", "required": false, "type": "input" } ] }
强约束(映射关系):
- 栏目 ext.fields[i].key 约定为内容 Content.ext 的 key
- 模板读取 ContentCardVM.Ext 时,key 必须与栏目 schema 的 key 一致
5. 常用子结构(供模板对照)
本节是便于主题模板对照的“最常用字段”,以 aithemes.md 中贴出的结构为准。
5.1 RequestData(模板常用:Request.Query)
- Request.Query:map[string]string
模板示例(搜索框回填):
value="{{.Request.Query.key}}"
5.2 PagerVM(分页)
模板侧建议:
- 优先遍历 .Pager.Items 渲染
- Kind == "gap" 时不要输出链接
- Disabled 时输出不可点样式
5.3 ContentCardVM(列表卡片 + 详情内容)
模板侧注意:
- 正文一般需要:{{.Content.Content|Str2Html}}
- 作者展示推荐:{{default .Content.Author .Content.User.Nickname}}
- 链接字段是 URL(不是 Url)
5.4 ColumnVM(.Single:栏目完整数据别名)
- .Single 是 *ColumnVM,内嵌了 *models.Column
- 模板可直接访问:{{.Single.Name}}、{{.Single.Slug}}、{{.Single.Description}}扩展:{{index .Single.Ext "xxx"}}
6. 主题开发渲染侧“严格遵守”清单
- 不要杜撰不存在的变量/方法;以 ViewModel 字段与 FuncMap 为准
- ContentCardVM 的链接字段使用 .URL(全大写),不是 .Url
- 建议对 Site 等对象做空判断再访问,避免渲染 panic
- map 取值使用 index,并配合 if 判断避免 <no value> 直接输出