Documentation
¶
Overview ¶
Package theme 提供主题系统:模板引擎、主题包加载、前台路由与主题设置。
主题是一个 zip 包(theme.yaml + settings.yaml + templates/ + static/),后台上传即可切换。 模板引擎用 html/template——主题是第三方代码,XSS 面就在主题里,而它是唯一做 上下文感知转义的引擎;模板作者体验由 layout / partial 约定与函数库补齐。
内置默认主题「墨 Ink」编译进二进制,既是开箱即用的外观,也是所有主题的回退: 第三方主题缺失可选模板时自动回退到它的同名模板。
Index ¶
- Constants
- Variables
- func AllowedExtensions() []string
- func ContentPath(kind, slug string) string
- func ErrTemplateMissing(err error) bool
- func IsNotFound(err error) bool
- func ListInstalled(root string) ([]string, error)
- func OptionalTemplates() []string
- func PageTemplates(names []string) []string
- func Remove(root, name string) error
- func RequiredTemplates() []string
- func RestoreBuiltin(root string, builtinFS fs.FS) error
- type Adjacent
- type ArchiveContext
- type ArchiveEntry
- type ArchivesFinder
- type AuthorView
- type BuiltinSync
- type CategoriesFinder
- type CategoryNode
- type CommentNode
- type CommentView
- type CommentsFinder
- type Context
- type CurrentUserView
- type Engine
- type Favoriter
- type FavoritesFinder
- type Finder
- type FormCSRFFunc
- type FormState
- type Frontend
- type Handler
- type Loaded
- type Manifest
- type MenuItemView
- type MenusFinder
- type Module
- func (m *Module) Frontend() *Frontend
- func (m *Module) Migrations() fs.FS
- func (m *Module) MountFrontend(r chi.Router, optional func(http.Handler) http.Handler)
- func (m *Module) Name() string
- func (m *Module) Navigation() app.Navigation
- func (m *Module) Permissions() []app.Permission
- func (m *Module) Register(a *app.App) error
- func (m *Module) Registry() *Registry
- func (m *Module) Renderer() *Renderer
- func (m *Module) RestoreBuiltin() error
- func (m *Module) Routes(r app.Router)
- func (m *Module) Start(ctx context.Context) error
- type Pagination
- type PluginWidget
- type PostView
- type PostsFinder
- func (p PostsFinder) Adjacent(postID int64, publishedAt time.Time) Adjacent
- func (p PostsFinder) ByCategory(slug string, n int) []PostView
- func (p PostsFinder) ByTag(slug string, n int) []PostView
- func (p PostsFinder) List(n int) []PostView
- func (p PostsFinder) Pinned(n int) []PostView
- func (p PostsFinder) Popular(n int) []PostView
- func (p PostsFinder) Recent(n int) []PostView
- func (p PostsFinder) Related(postID int64, n int) []PostView
- func (p PostsFinder) Total() int
- type Registry
- func (r *Registry) Activate(name string) error
- func (r *Registry) Active() *Loaded
- func (r *Registry) ActiveName() string
- func (r *Registry) AssetsHandler() http.Handler
- func (r *Registry) Broken() map[string]string
- func (r *Registry) DevMode() bool
- func (r *Registry) Fallback() *Loaded
- func (r *Registry) Get(name string) (*Loaded, bool)
- func (r *Registry) List() []*Loaded
- func (r *Registry) Load(name string) error
- func (r *Registry) LoadInstalled() error
- func (r *Registry) Reload(name string) error
- func (r *Registry) Unload(name string)
- func (r *Registry) Watch(ctx context.Context)
- type RegistryOptions
- type Renderer
- func (r *Renderer) Location(ctx context.Context) *time.Location
- func (r *Renderer) NewContext(ctx context.Context, req *http.Request, kind string) (*Context, error)
- func (r *Renderer) PageSize(ctx context.Context) int
- func (r *Renderer) Render(w http.ResponseWriter, req *http.Request, status int, name string, ...)
- func (r *Renderer) UseFormCSRF(fn FormCSRFFunc)
- type RendererOptions
- type SEOContext
- type Searcher
- type SettingsDecl
- type SettingsGroupDecl
- type SettingsStore
- type SiteContext
- type StateStore
- type Store
- func (s *Store) AdjacentPosts(ctx context.Context, postID int64, publishedAt time.Time) (prev, next *PostView, err error)
- func (s *Store) ArchivesByMonth(ctx context.Context) ([]ArchiveEntry, error)
- func (s *Store) ArchivesByYear(ctx context.Context) ([]ArchiveEntry, error)
- func (s *Store) Categories(ctx context.Context) ([]TermView, error)
- func (s *Store) CategoryTree(ctx context.Context) ([]*CategoryNode, error)
- func (s *Store) CommentTree(ctx context.Context, postID int64) ([]*CommentNode, error)
- func (s *Store) CountComments(ctx context.Context, postID int64) (int, error)
- func (s *Store) CountPosts(ctx context.Context) (int, error)
- func (s *Store) FavoritePosts(ctx context.Context, userID int64, page, size int) ([]PostView, int, error)
- func (s *Store) Favorites() Favoriter
- func (s *Store) GetAuthor(ctx context.Context, username string) (*AuthorView, error)
- func (s *Store) GetCategory(ctx context.Context, slug string) (*taxonomy.Category, error)
- func (s *Store) GetContent(ctx context.Context, kind, slug string, viewerID int64) (*PostView, error)
- func (s *Store) GetTag(ctx context.Context, slug string) (*taxonomy.Tag, error)
- func (s *Store) ListPosts(ctx context.Context, n int) ([]PostView, error)
- func (s *Store) Menu(ctx context.Context, slug string) ([]MenuItemView, error)
- func (s *Store) PageTemplatesInUse(ctx context.Context) ([]string, error)
- func (s *Store) PinnedPosts(ctx context.Context, n int) ([]PostView, error)
- func (s *Store) PopularPosts(ctx context.Context, n int) ([]PostView, error)
- func (s *Store) Posts(ctx context.Context, page, size int) ([]PostView, int, error)
- func (s *Store) PostsByArchive(ctx context.Context, year, month, page, size int) ([]PostView, int, error)
- func (s *Store) PostsByAuthor(ctx context.Context, authorID int64, page, size int) ([]PostView, int, error)
- func (s *Store) PostsByTerm(ctx context.Context, kind, slug string, page, size int) ([]PostView, int, error)
- func (s *Store) RecentComments(ctx context.Context, n int) ([]CommentView, error)
- func (s *Store) RecentPosts(ctx context.Context, n int) ([]PostView, error)
- func (s *Store) RelatedPosts(ctx context.Context, postID int64, n int) ([]PostView, error)
- func (s *Store) SearchPosts(ctx context.Context, query string, page, size int) ([]PostView, int, error)
- func (s *Store) Tags(ctx context.Context, limit int) ([]TermView, error)
- func (s *Store) UseExcerptFilter(fn func(string) string)
- func (s *Store) UseFavorites(favorites Favoriter)
- func (s *Store) UseSearcher(searcher Searcher)
- func (s *Store) UseTimezone(fn TimezoneFunc)
- type TagsFinder
- type TemplateStatus
- type TermView
- type ThemeContext
- type TimezoneFunc
- type View
Constants ¶
const ( KindIndex = "index" KindPost = "post" KindPage = "page" KindCategory = "category" KindTag = "tag" KindArchive = "archive" KindSearch = "search" KindAuthor = "author" KindNotFound = "404" // KindPosts 是全部文章的分页列表(/posts)。首页第一页可能摆的是模块, // 这里才是一篇不落的完整列表。 KindPosts = "posts" // KindFavorites 是「我的收藏」页。它由本模块渲染而不是 account—— // 那一页上是一列文章,而按可见性取文章、补作者分类标签、算分页 // 全都在本包里(account 不为内容提供任何接口)。 KindFavorites = "favorites" // 以下五种由 account 模块注入。 KindLogin = "login" KindRegister = "register" KindForgotPassword = "forgot-password" KindResetPassword = "reset-password" KindAccount = "account" )
路由种类。每种路由注入固定的、有文档的上下文, 保证主题作者「照着写就能跑」。
const ( // FileManifest 是主题元信息。 FileManifest = "theme.yaml" // FileSettings 是主题设置项声明,可缺省。 FileSettings = "settings.yaml" // DirTemplates 是模板目录。 DirTemplates = "templates" // DirStatic 是静态资源目录,可缺省。 DirStatic = "static" )
主题包内的固定文件名。
const ( // SourceEmbedded 表示这份内置主题直接来自二进制(磁盘上没有副本,或那份副本坏了)。 SourceEmbedded = "embedded" // SourceDisk 表示它是 data/themes/<内置主题名> 下那份副本:站长可以改,改了就生效。 SourceDisk = "disk" )
内置主题的两种来源。
const ( PathPosts = content.PostsPrefix PathCategories = "/categories/" PathTags = "/tags/" PathArchives = "/archives/" PathAuthors = "/authors/" PathSearch = "/search" // PathFavorites 是「我的收藏」页。 // // 挂在 /account 下而不是做成顶层的 /favorites:顶层单段路径会遮蔽同名的独立页面 // (已知限制,account 的那几条固定路径就是这么来的), // 而两段路径与 /{slug} 的兜底完全不相交,不必再往保留字清单里添一个词。 PathFavorites = "/account/favorites" // PathPostIndex 是全部文章列表。有同名独立页面时页面优先,见 Frontend.postIndex。 PathPostIndex = "/posts" )
前台路径前缀,与 internal/seo、internal/menu 的约定保持一致。
const AssetsPath = "/theme-assets"
AssetsPath 是主题静态资源的挂载前缀。
const BuiltinName = "ink"
BuiltinName 是内置默认主题的标识。
const EnvDevMode = "LUMO_THEME_DEV"
EnvDevMode 开启主题开发模式:模板改动自动重载、静态资源不缓存。
const Name = "theme"
Name 是模块名,也是迁移版本表的后缀(goose_db_version_theme)。
const Permission = perm.ThemesManage
Permission 是本模块相关操作所需的权限。
Variables ¶
var ( // ErrInvalidPackage 表示主题包不符合规范。 ErrInvalidPackage = errors.New("主题包不合法") // ErrTemplateNotFound 表示模板在主题与回退主题中都不存在。 ErrTemplateNotFound = errors.New("模板不存在") // ErrNotFound 表示主题未安装。 ErrNotFound = errors.New("主题不存在") // ErrAlreadyExists 表示同名主题已安装。 ErrAlreadyExists = errors.New("同名主题已存在") // ErrBuiltin 表示该操作不允许作用于内置主题。 ErrBuiltin = errors.New("内置主题不可删除") )
错误哨兵。
var CoverFiles = []string{"cover.webp", "cover.png", "cover.jpg"}
CoverFiles 是后台主题页展示的特色图(4:3,建议 1200×900),可缺省;按顺序取第一张存在的。
Functions ¶
func ContentPath ¶
ContentPath 返回内容的前台路径。
与 internal/seo 和 internal/menu 的约定保持一致:文章在 /posts/<slug>, 独立页面直接挂在根路径 /<slug>。
func ListInstalled ¶
ListInstalled 扫描 root 下已安装的主题目录名。
只列目录名不做校验:校验要解析全部模板,而列表接口可能被频繁调用。 具体主题是否可用由 Registry 在加载时判定并记录。
func PageTemplates ¶
PageTemplates 返回主题提供的 page-*.html 模板名。
独立页面可以在后台选择其中之一;列表由主题决定,Console 据此渲染下拉框。
Types ¶
type Adjacent ¶
Adjacent 返回上一篇与下一篇,供文章页的翻页链接用。
返回一个结构体而不是两个值:模板里 {{ $a := .Find.Posts.Adjacent $id }} 之后 再 {{ with $a.Prev }} 比解构两值更清楚地表达「可能没有上一篇」。
type ArchiveContext ¶
type ArchiveContext struct {
Year int
// Month 为 0 表示按年归档。
Month int
// Label 是可读标题,如「2026 年 9 月」。
Label string
}
ArchiveContext 描述归档页的时间区间。
type ArchiveEntry ¶
type ArchiveEntry struct {
Year int
// Month 为 0 表示按年归档。
Month int
Count int
// Label 是可读标题,如「2026 年 9 月」。
Label string
// URL 是归档页地址。
URL string
}
ArchiveEntry 是一个归档区间。
type ArchivesFinder ¶
type ArchivesFinder struct {
// contains filtered or unexported fields
}
ArchivesFinder 是 archives 命名空间。
func (ArchivesFinder) ByMonth ¶
func (a ArchivesFinder) ByMonth() []ArchiveEntry
ByMonth 返回按月归档的条目,新的在前。
func (ArchivesFinder) ByYear ¶
func (a ArchivesFinder) ByYear() []ArchiveEntry
ByYear 返回按年归档的条目,新的在前。
type AuthorView ¶
type AuthorView struct {
ID int64
Username string
DisplayName string
AvatarURL string
Bio string
// URL 是作者归档页地址。
URL string
}
AuthorView 是作者的公开信息。
邮箱不在其中:作者页是公开页面,把注册邮箱渲染上去等于给爬虫送礼。
type BuiltinSync ¶ added in v0.1.6
type BuiltinSync int
BuiltinSync 是启动时核对内置主题磁盘副本的结果。
const ( // BuiltinUnchanged 副本与二进制里的版本一致(或改过但二进制没出新版本),不需要动。 BuiltinUnchanged BuiltinSync = iota // BuiltinCreated 磁盘上还没有副本,刚解压了一份。 BuiltinCreated // BuiltinUpdated 副本没被改动过,已换成二进制里的新版本。 BuiltinUpdated // BuiltinModified 二进制里有新版本,但副本被改动过,原样保留。 BuiltinModified // BuiltinUnknown 副本是 0.1.5 及更早的版本解压的,没有出厂清单, // 也对不上任何一版出厂文件,判断不了改没改过,原样保留。 BuiltinUnknown )
func MaterializeBuiltin ¶
func MaterializeBuiltin(root string, builtinFS fs.FS) (BuiltinSync, error)
MaterializeBuiltin 让 root/<BuiltinName> 下有一份内置主题,并在程序升级后把没改过的副本换成新版本。
为什么要落到磁盘(2026-09-15 站长定):内置主题原先只活在二进制里, 于是 data/themes 空空如也 —— 站长既看不到默认主题长什么样,也改不动它, 而默认主题本该是一份普通主题:看得到、改得动。
落盘之后的规矩:**以磁盘为准**(缺的模板回退到二进制里的原版),后台不可删除, 改坏了走「恢复出厂」。
为什么升级时要换(2026-09-23):原先「已存在则不动」,副本就永远停在第一次启动的版本, 升级程序换不来主题的任何修复,0.1.5 的落地页模块在升上来的站点上根本不出现。 换不换只看一件事:磁盘上的文件是否与出厂清单逐字节一致,一致才换,多一个少一个都不换。
type CategoriesFinder ¶
type CategoriesFinder struct {
// contains filtered or unexported fields
}
CategoriesFinder 是 categories 命名空间。
func (CategoriesFinder) Tree ¶
func (c CategoriesFinder) Tree() []*CategoryNode
Tree 返回分类树,每个节点带文章数。
type CategoryNode ¶
type CategoryNode struct {
TermView
Children []*CategoryNode
}
CategoryNode 是带文章数的分类树节点。
type CommentNode ¶
type CommentNode struct {
CommentView
Children []*CommentNode
}
CommentNode 是评论树节点。
type CommentView ¶
type CommentView struct {
ID int64
AuthorName string
// AuthorURL 是访客填的主页,已校验过协议。
AuthorURL string
// ContentHTML 是转义并富化后的内容,模板里须用 safeHTML 输出。
ContentHTML template.HTML
CreatedAt time.Time
// IsAuthor 为真表示这条评论来自内容作者本人。
IsAuthor bool
// PostTitle 与 PostURL 仅在「最近评论」里填充。
PostTitle string
PostURL string
}
CommentView 是评论在模板中的视图。
字段刻意受限:邮箱、IP 与 UA 一律不进模板——主题是第三方代码, 一旦给了它就等于给了所有装这个主题的站点一个数据泄漏口子。
type CommentsFinder ¶
type CommentsFinder struct {
// contains filtered or unexported fields
}
CommentsFinder 是 comments 命名空间。
func (CommentsFinder) Count ¶
func (c CommentsFinder) Count(postID int64) int
Count 返回某条内容下已通过审核的评论数。
func (CommentsFinder) Recent ¶
func (c CommentsFinder) Recent(n int) []CommentView
Recent 返回站点最近通过审核的评论。
func (CommentsFinder) Tree ¶
func (c CommentsFinder) Tree(postID int64) []*CommentNode
Tree 返回某条内容下已通过审核的评论树。
type Context ¶
type Context struct {
// Kind 是当前路由种类,取值见 Kind* 常量。模板可据此在 partial 里分支。
Kind string
// Site 是站点级信息,每个页面都有。
Site SiteContext
// Theme 是当前主题的信息与设置值。
Theme ThemeContext
// Title 是本页标题(不含站点标题后缀),主题自行拼接。
Title string
// Description 是本页描述,供 meta 使用。
Description string
// Canonical 是本页的绝对地址;站点未配置对外地址时为相对路径。
Canonical string
// SEO 是本页的 SEO 派生信息,由 Renderer 按站点设置与本页内容算好。
SEO SEOContext
// Path 是当前请求路径。
Path string
// Post 在文章页与独立页面上为当前内容,其余为 nil。
Post *PostView
// Posts 是列表类页面(首页、分类、标签、归档、搜索、作者)的内容列表。
Posts []PostView
// Pagination 是列表类页面的翻页信息。
Pagination Pagination
// Category 在分类页上为当前分类。
Category *taxonomy.Category
// Tag 在标签页上为当前标签。
Tag *taxonomy.Tag
// Author 在作者页上为当前作者。
Author *AuthorView
// Archive 在归档页上为当前归档区间。
Archive *ArchiveContext
// Query 在搜索页上为搜索词。
Query string
// Find 是只读数据访问面,供侧栏页脚一类小部件取数。
//
// 为什么挂在上下文上而不是做成 posts.recent 这样的顶层模板函数:
// 模板函数在解析期就固定了,而 Finder 必须绑定当次请求的 context
// (超时、取消、请求内缓存都依赖它)。要让顶层函数拿到请求 context,
// 只能每个请求 template.Clone 一份——而 html/template 的转义分析结果
// 缓存在模板实例上,克隆意味着每个请求都重跑一遍全量转义分析,
// 这是前台每次访问都要付的代价。故改为 {{ .Find.Posts.Recent 5 }}。
Find *Finder
// Params 是路由未消费的额外参数,供主题自行取用。
Params map[string]string
// Public 是各设置分组中标为公开的字段值,按分组名索引,
// 如 account.allowRegistration。模板里用 .Setting 取,不要直接索引。
//
// 为什么主题需要它:页眉要显示「注册」入口,就得知道注册是不是开着,
// 而页眉出现在**每一个**页面上——由账户模块渲染时逐页塞进 Params 是不行的。
// 取值走 settings 分组的 Public 白名单(与 Public 平面接口同一份声明),
// 主题拿不到未公开的字段。
Public map[string]map[string]any
// CurrentUser 是当前登录用户,匿名时为 nil。
//
// 只放前台该知道的字段:邮箱与验证时间不在其中,页眉不该把用户的注册邮箱渲染出来。
// 由 Renderer.NewContext 从 auth 的 principal 填充——根路由上没有鉴权中间件,
// 没有 auth.Authenticator.Optional 挂在前台,这个字段就恒为 nil。
CurrentUser *CurrentUserView
// CSRFToken 是本页可用的表单令牌,供**页面自带**的表单使用——
// 目前只有页眉账户菜单里的退出登录那一张。
//
// 只对已登录访客签发(匿名页因此仍然可以被共享缓存),由 Renderer.Render
// 在渲染前填入;没有接上签发钩子时为空串,模板据此不渲染那张表单。
// 表单页(登录、注册、账户等)请用 .Form.CSRFToken,那是同一枚令牌。
CSRFToken string
// Form 是表单页的状态:回填值、逐字段错误、提示与令牌。
//
// 做成显式结构而不是塞进 Params:值与错误混在一个 map 里,
// 主题作者要靠键名前缀区分,那是约定而不是契约。
Form *FormState
// contains filtered or unexported fields
}
Context 是注入模板的根对象,模板里以 `.` 访问。
所有路由共用一个结构体而非每种路由一个类型:模板作者只需记住一套字段名, 且 partial 可以无差别地接收任何页面的上下文(页头页脚要用 Site 与 Menus, 而它们在每种页面上都得在)。与具体路由相关的字段按需填充,其余为零值。
func (*Context) Setting ¶
Setting 读取某个设置分组的公开字段值;分组未注册或字段未公开时返回 nil。
做成方法而不是让模板写 {{ index (index .Public "account") "allowRegistration" }}: 嵌套索引在模板里读不出意图,而且少写一层就会撞上「nil map 没有这个键」的渲染错误。 缺键返回 nil 而不是报错是有意的——第三方主题引用一个本项目不存在的分组时, 该少显示一个链接,而不是让整站 500。
func (*Context) Slot ¶ added in v0.2.0
Slot 输出插件放进某个插槽的内容:head、footer、content.before、content.after、comments.after。
主题在对应位置各调一次,如 {{ .Slot "head" }} 放在 </head> 之前、{{ .Slot "footer" }} 放在 </body> 之前。 插件的样式表与脚本也是经 head 与 footer 两个插槽放进来的;主题不调它们,插件就进不了前台。 内容已由插件模块净化,原样输出即可。
func (*Context) Widget ¶ added in v0.2.0
func (c *Context) Widget(id string) *PluginWidget
Widget 渲染插件提供的侧栏小组件,id 形如 <插件>/<小组件>;插件没启用或没有内容时为 nil。
type CurrentUserView ¶
type CurrentUserView struct {
ID int64
Username string
DisplayName string
AvatarURL string
// ConsoleAccess 为真表示该账号至少有一条权限,页眉可以显示「进入后台」。
//
// 判据是「有没有权限」而不是「角色名是不是管理员」:自定义角色同样可能持有权限,
// 而 member 这类零权限账号点进后台只会得到一屏 403。
ConsoleAccess bool
}
CurrentUserView 是注入模板的当前登录用户视图。
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine 是一套已解析的主题模板。
用 html/template 而非 pongo2 / jet:主题是第三方代码,XSS 面就在主题里, 只有 html/template 做上下文感知转义。代价是模板作者体验一般, 用 layout / partial 约定与函数库补齐。
并发安全:解析在构造期一次完成,Render 只读。
func (*Engine) Render ¶
Render 渲染指定模板。
模板缺失时回退到内置默认主题的同名模板,而不是报错或渲染空白—— 分类、标签、归档、搜索、作者这五个模板对主题作者是可选的。
注意回退是**整页**回退而非按块回退:缺 category.html 时整页都由默认主题渲染, 页头页脚也跟着变。按块混合会得到一个两种设计语言拼起来的页面,比整页回退更糟。
func (*Engine) SetFallback ¶
SetFallback 设置回退引擎:本主题缺失的模板转由它渲染。
type Favoriter ¶
type Favoriter interface {
// FavoritePostIDs 按收藏时间倒序返回某人收藏的、当前仍对访客可见的内容 ID 与总数。
FavoritePostIDs(ctx context.Context, userID int64, limit, offset int) ([]int64, int, error)
// HasFavorite 报告某人是否收藏过某篇内容。
HasFavorite(ctx context.Context, userID, postID int64) (bool, error)
// CountFavorites 返回一篇内容被收藏的次数。
CountFavorites(ctx context.Context, postID int64) (int, error)
}
Favoriter 是收藏页与文章页收藏按钮需要的能力,由 internal/favorite 实现。
与 Searcher 同一种分工:收藏模块只回答「谁收了哪几篇」「这篇被收了多少次」, 出的是 ID 与计数;列表上那一列文章由本包自己的查询补齐。 主题因此不必知道收藏存在哪张表,收藏模块也不必知道前台怎么展示一篇文章。
type FavoritesFinder ¶
type FavoritesFinder struct {
// contains filtered or unexported fields
}
FavoritesFinder 是 favorites 命名空间。
func (FavoritesFinder) Enabled ¶
func (v FavoritesFinder) Enabled() bool
Enabled 报告站点是否装配了收藏功能。
主题据此决定画不画收藏按钮:没装配收藏模块时,一个点了必然报错的按钮 比没有这个按钮更糟。
func (FavoritesFinder) Has ¶
func (v FavoritesFinder) Has(postID int64) bool
Has 报告**当前登录用户**是否收藏过这篇内容;匿名访客恒为 false。
用户 ID 从请求上下文里取而不是让模板传:模板里根本没有可信的用户 ID 可传, 传进来的任何值都等于让主题替别人查收藏状态。
type Finder ¶
type Finder struct {
// contains filtered or unexported fields
}
Finder 是供模板调用的只读数据访问面。
刻意**不提供任意查询**:主题能调的每个函数都在这里逐个列出、各自带缓存与条数上限。 开放一个 `query` 函数会让主题变成应用——慢查询、N+1、越权读取都会跟着进来, 而主题是第三方代码,出了问题排查成本落在站长身上。
每个 Finder 绑定一次请求:ctx 随请求取消,缓存只在本次渲染内共享。
func (*Finder) Categories ¶
func (f *Finder) Categories() CategoriesFinder
Categories 返回 categories 命名空间。
func (*Finder) Favorites ¶
func (f *Finder) Favorites() FavoritesFinder
Favorites 返回 favorites 命名空间。
func (*Finder) Posts ¶
func (f *Finder) Posts() PostsFinder
Posts 返回 posts 命名空间,模板里写 {{ posts.Recent 5 }}。
type FormCSRFFunc ¶
type FormCSRFFunc func(w http.ResponseWriter, r *http.Request) string
FormCSRFFunc 为本次响应准备一枚表单 CSRF 令牌并返回明文。
由 account 模块实现(它拥有前台表单的那套双提交),serve 在两个模块都装配完之后 接上去。做成函数而不是让本包引 account:account 用本包的 Renderer 渲染账户页, 反过来引就是一个导入环。
type FormState ¶
type FormState struct {
// Values 是回填值,绝不含任何密码字段。
Values map[string]string
// Errors 的键是字段名,值是给用户看的中文原因。
Errors map[string]string
// Notice 是页面级提示(成功或失败),空串表示不显示。
Notice string
// NoticeKind 取值 "ok" 或 "error",模板据此选样式。
NoticeKind string
// CSRFToken 要渲染进表单的 hidden 字段。
CSRFToken string
// Token 是重置密码页要把邮件里的令牌带回 POST 的隐藏字段。
Token string
}
FormState 是表单页的渲染状态。
密码字段**永远不进 Values**:回填密码意味着它会被写进 HTML, 而 HTML 会进浏览器缓存、会被「查看源代码」看到、会被截图带出去。
func NewFormState ¶
func NewFormState() *FormState
NewFormState 构造一个空的表单状态,映射已初始化。
让 account 模块不必每处都手写 make:漏掉一个,模板里的 .Form.Values.xxx 就会 报「nil map」而不是渲染成空值。
type Frontend ¶
type Frontend struct {
// contains filtered or unexported fields
}
Frontend 是访客前台的路由处理器。
前台由 Go 服务端模板渲染,不经三平面:它产出的是 HTML 页面 而非 JSON,鉴权模型也不同(匿名可读,登录用户额外看得到自己的私密内容)。
func NewFrontend ¶
NewFrontend 构造前台处理器;events 为 nil 时正文不经插件过滤。
func (*Frontend) Mount ¶
Mount 把前台路由挂到根路由上。
路由顺序要紧:/posts/{slug} 这类带固定前缀的必须先注册, 最后才是兜底的 /{slug}(独立页面),否则页面路由会吞掉一切。
func (*Frontend) NotFoundHandler ¶
func (f *Frontend) NotFoundHandler() http.HandlerFunc
NotFoundHandler 返回主题渲染的 404 处理器,供根路由在无匹配时使用。
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler 提供主题的 Console 管理接口。
type Loaded ¶
type Loaded struct {
// Manifest 是主题元信息。
Manifest Manifest
// Builtin 为真表示这是内置主题:不可删除,且始终是回退链的末端。
// 它可能是从二进制里直接加载的,也可能是磁盘上那份解压出来的副本(见 Source)。
Builtin bool
// Source 说明内置主题从哪来:SourceEmbedded 或 SourceDisk;非内置主题为空串。
Source string
// Dir 是主题目录的绝对路径;来自二进制的内置主题为空串。
Dir string
// FS 是主题包的根文件系统。
FS fs.FS
// Static 是 static 子目录的文件系统;主题没有 static 时为 nil。
Static fs.FS
// AssetVersion 是静态资源的内容指纹,模板把它挂在资源 URL 的查询串上。
// 主题文件一变它就变,浏览器的 1 小时缓存因此不会在改版后继续端出旧样式。
AssetVersion string
// Templates 是模板名列表(主题自己提供的)。
Templates []string
// contains filtered or unexported fields
}
Loaded 是一个已加载并可用的主题。
func (*Loaded) SettingGroups ¶
func (l *Loaded) SettingGroups() []*compiledGroup
SettingGroups 返回主题声明的设置分组。
type Manifest ¶
type Manifest struct {
// Name 是主题标识(DNS-1123),同时是安装目录名与 URL 片段,必须与目录名一致。
Name string `yaml:"name" json:"name"`
// Label 是显示名。
Label string `yaml:"label" json:"label"`
// Version 是主题版本。
Version string `yaml:"version" json:"version"`
// Description 是一句话说明。
Description string `yaml:"description" json:"description"`
// Author 是作者名。
Author string `yaml:"author" json:"author"`
// Homepage 是主题主页。
Homepage string `yaml:"homepage" json:"homepage"`
// Repo 是源码仓库地址。
Repo string `yaml:"repo" json:"repo"`
// License 是许可证标识。
License string `yaml:"license" json:"license"`
// RequireLumo 声明所需的最低 Lumo 版本,形如 0.1.0;留空表示不限制。
RequireLumo string `yaml:"requireLumo" json:"requireLumo"`
}
Manifest 是 theme.yaml 的内容。
只认 YAML:主题作者写 settings.yaml 已经要用 YAML,没必要再引入第二种格式。
type MenuItemView ¶
type MenuItemView struct {
Label string
URL string
// Target 为 _blank 时在新窗口打开。
Target string
Rel string
// Children 是子条目。
Children []MenuItemView
}
MenuItemView 是菜单条目在模板中的视图。
type MenusFinder ¶
type MenusFinder struct {
// contains filtered or unexported fields
}
MenusFinder 是 menus 命名空间。
func (MenusFinder) Get ¶
func (m MenusFinder) Get(slug string) []MenuItemView
Get 按 slug 取菜单树,只含可见且目标存在的条目。
菜单不存在时返回空切片而不是报错:主题引用了站长还没建的菜单是常见情形, 该显示的是「没有导航」,不是一个 500 页面。
type Module ¶
type Module struct {
// contains filtered or unexported fields
}
Module 是主题模块。
func (*Module) MountFrontend ¶
MountFrontend 把访客前台路由与主题静态资源挂到根路由上。
由 serve 在全部模块注册之后调用:前台的兜底路由 /{slug} 会吞掉根路径下的 一切单段路径,必须排在 /console/、/uploads/ 与 SEO 文档之后注册。
直接接受 chi.Router 而非自定义接口:前台要用 chi.URLParam 读路径参数, 本就与 chi 的路由树绑定,抽象一层只会掩盖这个事实。
optional 是「解析会话但不强制认证」的中间件(auth.Authenticator.Optional)。 页面的登录态要靠它才进得来——unlike 三平面,根路由上没有鉴权中间件, 没有它时 routes.go 的 viewerID 恒为 0,作者点自己文章的链接会看到 404。 传 nil 表示不解析会话(测试里的纯渲染场景)。
func (*Module) Navigation ¶
func (m *Module) Navigation() app.Navigation
Navigation 实现 app.NavigationProvider。
主题的**全部**操作都要求 themes:manage(含列表)——主题能执行任意模板逻辑并决定 整站外观,门槛与设置同级,故未持有时整项隐藏,点进去也只会得到 403。
func (*Module) Permissions ¶
func (m *Module) Permissions() []app.Permission
Permissions 实现 app.PermissionProvider。
themes:manage 已由核心声明(app.CorePermissions),此处不重复声明, 否则权限清单里会出现两条同名项。
func (*Module) Register ¶
Register 实现 app.Module:只做装配,不访问数据库。
注意主题注册表在此构造:它要加载内置主题并解析模板,是纯文件系统操作, 与数据库无关。启用哪个主题要读库,那在 Start 里做。
func (*Module) Renderer ¶
Renderer 返回页面渲染器。
导出给 account 模块:前台账户页与全站页面必须用同一套模板引擎、同一套回退规则与 同一份主题设置,否则账户页会在换主题时独自错位。它是本模块唯一对外开放的 渲染入口,模块之间仍然不引用彼此的内部实现。
func (*Module) RestoreBuiltin ¶
RestoreBuiltin 把内置主题恢复成出厂状态(重新解压)并立即重新加载。
站长改坏了模板时这是回到原版的唯一入口 —— 没有它,就只能去翻发行包。 调用方必须先确认:站长对内置主题的改动会全部丢失。
type Pagination ¶
type Pagination struct {
// Page 是当前页码,从 1 起。
Page int
// Size 是每页条数。
Size int
// Total 是总条数。
Total int
// TotalPages 是总页数,至少为 1。
TotalPages int
// HasPrev / HasNext 表示是否存在上一页 / 下一页。
HasPrev bool
HasNext bool
// PrevURL / NextURL 是上一页与下一页的地址,不存在时为空串。
PrevURL string
NextURL string
// BaseURL 是本列表的基地址,主题拼页码时用,如 /categories/go。
BaseURL string
}
Pagination 是列表页的翻页信息。
字段全部预先算好而不是让模板自己算:分页边界是最容易写错的地方, 每个主题各算一遍必然各错一遍。
func (*Pagination) PageURL ¶
func (p *Pagination) PageURL(page int) string
PageURL 返回列表某一页的地址,供模板渲染页码条。
func (*Pagination) Pages ¶
func (p *Pagination) Pages() []int
Pages 返回页码条上应显示的页码,超长时以 0 表示省略号。
预先算好而不是丢给模板:省略号的边界条件(首尾保留、当前页居中、 总页数少于窗口时不省略)是典型的「每个人都会写错一次」的逻辑。
type PluginWidget ¶ added in v0.2.0
PluginWidget 是一个插件小组件渲染出的内容。
type PostView ¶
type PostView struct {
ID int64
Type string
Title string
Slug string
// URL 是本内容的站内路径,如 /posts/hello。
URL string
// Content 是渲染后的 HTML。模板里须用 safeHTML 输出。
Content string
Excerpt string
// CoverURL 是封面图地址,可能为空。
CoverURL string
Pinned bool
// Template 是页面选择的主题模板名(仅独立页面),如 page-about。
Template string
Author *AuthorView
Categories []taxonomy.Category
// Tags 用 TermView 而不是 taxonomy.Tag:它要带一个派生出来的色相(Hue),
// 而色相是「怎么画」的事,不该塞进核心的领域模型里。
Tags []TermView
PublishedAt time.Time
UpdatedAt time.Time
// ReadingTime 是估算阅读分钟数。
ReadingTime int
// WordCount 是正文字数(CJK 按字、拉丁按词)。
WordCount int
// contains filtered or unexported fields
}
PostView 是注入模板的内容视图。
不直接用 content.Post:那是数据库实体,带着 Raw 原稿与 Meta 这些主题不该碰的字段。 主题只消费渲染后的 Content,编辑器换了主题也不受影响。
type PostsFinder ¶
type PostsFinder struct {
// contains filtered or unexported fields
}
PostsFinder 是 posts 命名空间。
func (PostsFinder) Adjacent ¶
func (p PostsFinder) Adjacent(postID int64, publishedAt time.Time) Adjacent
Adjacent 返回与给定文章相邻的两篇(按发布时间)。
文章没有发布时间(草稿不该走到这里,但模板可能被别处调用)时返回两个 nil, 模板的 {{ with }} 会自然跳过。
func (PostsFinder) ByCategory ¶
func (p PostsFinder) ByCategory(slug string, n int) []PostView
ByCategory 返回某个分类下的文章。
func (PostsFinder) ByTag ¶
func (p PostsFinder) ByTag(slug string, n int) []PostView
ByTag 返回某个标签下的文章。
func (PostsFinder) List ¶ added in v0.1.9
func (p PostsFinder) List(n int) []PostView
List 按前台列表的顺序返回前 n 篇:置顶在前,再按发布时间倒序。
首页「最新文章」这类主列表用它,与 /posts、分类页的顺序一致;侧栏的「最新」仍用 Recent,只看时间。
func (PostsFinder) Popular ¶
func (p PostsFinder) Popular(n int) []PostView
Popular 返回按评论数排序的热门文章(判据见 Store.PopularPosts)。
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry 持有全部已加载主题与当前启用项。
它是主题系统的唯一运行时状态:路由、Console 接口与热重载都经它取主题。 读远多于写(每个前台请求都要读一次启用主题),故用 RWMutex。
func NewRegistry ¶
func NewRegistry(opts *RegistryOptions) (*Registry, error)
NewRegistry 构造 Registry 并加载内置主题。
内置主题必须加载成功,否则整个主题系统没有回退可用—— 那意味着任何第三方主题缺一个可选模板就会 500。故此处失败即启动失败。 这里加载的**始终是二进制里那一份**:它是回退链的末端。 磁盘上的副本由 LoadInstalled 顶上来(见 loadBuiltinFromDisk)。
func (*Registry) AssetsHandler ¶
AssetsHandler 提供主题的 static 目录,挂在 AssetsPath 下。
路径形如 /theme-assets/<主题名>/<文件>:带上主题名而不是只服务当前启用主题, 是为了让后台预览其他主题时资源也能正确加载。
func (*Registry) Load ¶
Load 加载(或重新加载)一个已安装主题。
加载内置主题名(data/themes/<BuiltinName>)时,结果仍然是**内置**主题: 不可删除、排在列表最前、当别的主题的回退目标,只是来源标成磁盘。 这一点必须在这里保证:「重新载入模板」走的也是这个方法, 漏掉就会被悄悄降级成一个可以卸载的普通主题。
func (*Registry) LoadInstalled ¶
LoadInstalled 扫描并加载 root 下已安装的主题。
单个主题加载失败不影响其余:一个坏主题不该让整站起不来, 它会被记进 broken 并在后台列表里标出原因。
type RegistryOptions ¶
type RegistryOptions struct {
// Root 是主题安装目录,通常是 data/themes。
Root string
// Builtin 是内置默认主题的文件系统(已剥到主题包根)。
Builtin fs.FS
// Logger 记录加载与重载事件。
Logger *slog.Logger
// DevMode 为真时每次渲染前检查模板改动并自动重解析。
DevMode bool
}
RegistryOptions 是构造 Registry 的参数。
type Renderer ¶
type Renderer struct {
// contains filtered or unexported fields
}
Renderer 把路由上下文渲染成 HTML 响应。
func (*Renderer) NewContext ¶
func (r *Renderer) NewContext(ctx context.Context, req *http.Request, kind string) (*Context, error)
NewContext 组装一个页面上下文的公共部分。
func (*Renderer) Render ¶
func (r *Renderer) Render(w http.ResponseWriter, req *http.Request, status int, name string, ctx *Context)
Render 渲染一个页面上下文并写出响应。
渲染先写到内存缓冲再一次性输出:模板执行到一半才出错时, 直接写 ResponseWriter 会留下半个页面且已经发了 200,客户端无从判断。
func (*Renderer) UseFormCSRF ¶
func (r *Renderer) UseFormCSRF(fn FormCSRFFunc)
UseFormCSRF 接上表单令牌的签发钩子。
type RendererOptions ¶
type RendererOptions struct {
Registry *Registry
Store *Store
Settings *settings.Service
Themes *SettingsStore
Logger *slog.Logger
// Plugins 是插件的前台入口,可为 nil。
Plugins app.Frontend
}
RendererOptions 是构造 Renderer 的参数。
type SEOContext ¶ added in v0.1.1
type SEOContext struct {
// TitleSuffix 是 SEO 设置里的标题后缀,主题拼在 <title> 之后。
TitleSuffix string
// Image 是本页分享图的绝对地址;内容没有封面时回落到默认分享图。
Image string
// TwitterSite 是站点的 Twitter 账号(含 @),为空则不渲染 twitter:site。
TwitterSite string
// JSONLD 是本页的 schema.org 结构化数据,已序列化;没有数据时为空。
//
// 类型是 template.JS,模板里直接输出进 <script type="application/ld+json">。
// 内容经 encoding/json 序列化,其中的尖括号与 & 已转成 Unicode 转义形式。
JSONLD template.JS
}
SEOContext 是 SEO 设置与结构化数据在模板里的出口。
主题不必自己去 .Public.seo 里挖那几项再做回落:标题后缀、默认描述、 默认分享图的优先级规则是站点行为,不该由每个主题各写一遍。
type Searcher ¶
type Searcher interface {
SearchPostIDs(ctx context.Context, query string, limit, offset int) ([]int64, int, error)
}
Searcher 是前台搜索页需要的能力:关键词进,按相关度排好序的文章 ID 与总数出。
只要 ID 与顺序,作者 / 分类 / 标签仍由主题自己的查询补齐——这样搜索引擎换成 什么实现都与前台无关。
type SettingsDecl ¶
type SettingsDecl struct {
// Groups 是设置分组列表。
Groups []SettingsGroupDecl `yaml:"groups" json:"groups"`
}
SettingsDecl 是 settings.yaml 的内容:主题自己的设置项声明。
复用站点设置的统一表单 Schema:Console 用同一个表单引擎渲染, 主题作者只需学一次。与站点设置的唯一差别是它不走 app.SettingGroup 的启动期登记—— 主题可以随时换,分组不能钉死在进程启动那一刻。
func (*SettingsDecl) SettingGroups ¶
func (d *SettingsDecl) SettingGroups() ([]app.SettingGroup, error)
SettingGroups 把主题声明转成与站点设置同构的分组,供表单引擎与校验器使用。
返回 app.SettingGroup 而非自定义类型:Console 的通用表单引擎只认这一种形态, 主题设置与站点设置在前端走完全相同的渲染路径。
type SettingsGroupDecl ¶
type SettingsGroupDecl struct {
Name string `yaml:"name" json:"name"`
Label string `yaml:"label" json:"label"`
Description string `yaml:"description" json:"description"`
Order int `yaml:"order" json:"order"`
// Schema 是 JSON Schema 2020-12 子集 + x-widget,以 YAML 书写。
Schema map[string]any `yaml:"schema" json:"schema"`
// Defaults 是缺省值对象,须能通过 Schema 校验。
Defaults map[string]any `yaml:"defaults" json:"defaults"`
}
SettingsGroupDecl 是主题设置的一个分组。
type SettingsStore ¶
type SettingsStore struct {
// contains filtered or unexported fields
}
SettingsStore 持久化主题设置值。
与站点设置分表:主题设置随主题走,卸载主题时要能连同它的设置一起清掉, 而站点设置是核心资产。混在一张表里迟早会误删。
func NewSettingsStore ¶
func NewSettingsStore(db *bun.DB) *SettingsStore
NewSettingsStore 构造 SettingsStore。
type SiteContext ¶
type SiteContext struct {
Title string
Subtitle string
Description string
// URL 是站点对外地址(不含尾斜杠);未配置时为空串。
URL string
Language string
LogoURL string
FaviconURL string
// Now 是渲染时刻,已按站点时区换算,供页脚年份一类用途。
Now time.Time
// Generator 是生成器标识,形如 Lumo 1.0.0。
Generator string
}
SiteContext 是站点级信息,取自 site 设置分组。
type StateStore ¶
type StateStore struct {
// contains filtered or unexported fields
}
StateStore 持久化「当前启用的是哪个主题」。
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store 提供前台渲染所需的只读查询。
全部查询都内建「已发布 + 公开」的过滤:前台不存在「忘了加 where」导致 草稿泄漏的可能——那是 CMS 最不能犯的错。私密内容对作者本人的可见性 由路由层单独处理,不走这里。
func (*Store) AdjacentPosts ¶
func (s *Store) AdjacentPosts(ctx context.Context, postID int64, publishedAt time.Time) (prev, next *PostView, err error)
AdjacentPosts 返回给定文章的上一篇与下一篇(按发布时间,同在公开范围之内)。
比较用 (published_at, id) 这个二元组而不是只看时间:同一秒发布的两篇文章 用单列比较会出现「上一篇和下一篇是同一篇」或者干脆漏掉一篇。
草稿、私密与定时未到的文章不算数 —— 前后篇是给读者在公开内容里翻页用的, 让他翻到一篇打不开的页面,比没有这个链接更糟。
func (*Store) ArchivesByMonth ¶
func (s *Store) ArchivesByMonth(ctx context.Context) ([]ArchiveEntry, error)
ArchivesByMonth 返回按月归档,新的在前。
func (*Store) ArchivesByYear ¶
func (s *Store) ArchivesByYear(ctx context.Context) ([]ArchiveEntry, error)
ArchivesByYear 返回按年归档,新的在前。
func (*Store) Categories ¶
Categories 返回平铺的分类列表,每个带文章数。
func (*Store) CategoryTree ¶
func (s *Store) CategoryTree(ctx context.Context) ([]*CategoryNode, error)
CategoryTree 返回带文章数的分类树。
func (*Store) CommentTree ¶
CommentTree 返回某条内容下已通过审核的评论树。
func (*Store) CountComments ¶
CountComments 返回某条内容下已通过审核的评论数。
func (*Store) CountPosts ¶
CountPosts 返回已发布且公开的文章总数。
func (*Store) FavoritePosts ¶
func (s *Store) FavoritePosts(ctx context.Context, userID int64, page, size int) ([]PostView, int, error)
FavoritePosts 分页返回某人收藏的内容,按收藏时间倒序。
未装配收藏模块时返回空列表而不是错误:收藏页在那种装配下仍然打得开, 只是永远是空的——这与搜索页在没有 search 模块时退回模糊匹配是同一条思路。
func (*Store) GetCategory ¶
GetCategory 按 slug 取分类。
func (*Store) GetContent ¶
func (s *Store) GetContent(ctx context.Context, kind, slug string, viewerID int64) (*PostView, error)
GetContent 按类型与 slug 取一条内容(含正文)。
viewerID 非 0 时额外放行该用户自己的私密内容——作者预览自己的私密文章 不该看到 404。草稿与回收站内容一律不可见,预览走 Console。
匿名访客看到的版本进共享缓存。交出去的是副本:渲染时插件与短代码会改写正文。
func (*Store) PageTemplatesInUse ¶
PageTemplatesInUse 返回内容表里实际用到的页面模板名,供后台校验。
func (*Store) PinnedPosts ¶
PinnedPosts 返回置顶文章。
func (*Store) PopularPosts ¶
PopularPosts 返回按评论数排序的热门文章。// PopularPosts 返回按评论数排序的热门文章。
判据只有**已通过审核的评论数**:本站不记录浏览量,也不做「阅读数」这种要额外埋点、 又能被刷的东西。评论数是现成的、需要人工审核才能涨的,做「热门」的判据正好。 评论数相同的按发布时间倒序,保证结果稳定(同分时不能每次刷新都换一批)。
页面的评论不参与:热门是文章列表,页面(关于、联系)本就不该出现在首页模块里。
func (*Store) PostsByArchive ¶
func (s *Store) PostsByArchive(ctx context.Context, year, month, page, size int) ([]PostView, int, error)
PostsByArchive 分页返回某年(月)的文章。month 为 0 表示整年。
func (*Store) PostsByAuthor ¶
func (s *Store) PostsByAuthor(ctx context.Context, authorID int64, page, size int) ([]PostView, int, error)
PostsByAuthor 分页返回某作者的文章。
func (*Store) PostsByTerm ¶
func (s *Store) PostsByTerm(ctx context.Context, kind, slug string, page, size int) ([]PostView, int, error)
PostsByTerm 分页返回某个分类或标签下的文章。
func (*Store) RecentComments ¶
RecentComments 返回站点最近通过审核的评论。
func (*Store) RecentPosts ¶
RecentPosts 返回最近发布的文章。
func (*Store) RelatedPosts ¶
RelatedPosts 返回与给定文章共享分类或标签最多的其他文章。
按共享词条数排序而非随机:共享 3 个标签的文章显然比共享 1 个的更相关。
func (*Store) SearchPosts ¶
func (s *Store) SearchPosts(ctx context.Context, query string, page, size int) ([]PostView, int, error)
SearchPosts 分页返回命中关键词的文章,按相关度排序。
装配了 search 模块时走全文索引;否则退回标题与摘要的 ILIKE 模糊匹配, 保证主题在最小装配下仍有一个能用的搜索页。
func (*Store) UseExcerptFilter ¶ added in v0.2.0
UseExcerptFilter 注入摘要的加工函数:摘要是从正文取的纯文字,插件的短代码在里面只是一串方括号。
func (*Store) UseFavorites ¶
UseFavorites 注入收藏实现。
不注入时收藏页是空的、文章页不显示收藏按钮(见 Finder.Favorites 与 routes.go)—— 收藏模块没装配就等于站点没有这个功能,而不是一个点了报错的按钮。
func (*Store) UseSearcher ¶
UseSearcher 注入搜索实现;不注入时搜索页退回标题与摘要的模糊匹配。
func (*Store) UseTimezone ¶ added in v0.1.1
func (s *Store) UseTimezone(fn TimezoneFunc)
UseTimezone 注入站点时区来源;不注入时一切时间按 UTC 渲染。
type TagsFinder ¶
type TagsFinder struct {
// contains filtered or unexported fields
}
TagsFinder 是 tags 命名空间。
func (TagsFinder) Cloud ¶
func (t TagsFinder) Cloud(n int) []TermView
Cloud 返回按文章数倒序的前 n 个标签。
顺序就是热度的唯一表达:字号不参与编码(见 withHues)。
type TemplateStatus ¶
type TemplateStatus struct {
Name string `json:"name"`
// Required 为真表示这是四个必需模板之一。
Required bool `json:"required"`
// Provided 为真表示主题自己提供了它;否则渲染时回退到默认主题。
Provided bool `json:"provided"`
}
TemplateStatus 描述一个模板在主题中的可用情况,供后台展示。
func TemplateStatuses ¶
func TemplateStatuses(names []string) []TemplateStatus
TemplateStatuses 汇总必需与可选模板的提供情况。
type TermView ¶
type TermView struct {
ID int64
Name string
Slug string
Description string
// Color 仅标签有值。
Color string
// CoverURL 仅分类有值。
CoverURL string
// URL 是该分类或标签的归档页地址。
URL string
// Count 是其下已发布且公开的文章数。
Count int
// Hue 是这枚标签的色相(0–359),CSS 用它拼出底色与文字色。
// 站长设过颜色就用那个颜色的色相,没设过由标识稳定算出(见 tagHue)。
// 分类不用它,为零值。
Hue int
}
TermView 是分类或标签在模板中的视图,附带文章数。
type ThemeContext ¶
type ThemeContext struct {
Name string
Label string
Version string
// Settings 是主题设置的有效值,按分组名索引,如 .Theme.Settings.appearance.accentColor。
Settings map[string]map[string]any
// AssetsBase 是本主题静态资源的访问前缀,如 /themes/ink/static。
AssetsBase string
// AssetsVersion 是静态资源指纹,模板挂在 URL 上做缓存失效(见 Loaded.AssetVersion)。
AssetsVersion string
}
ThemeContext 是当前主题的信息与设置值。
type TimezoneFunc ¶ added in v0.1.1
TimezoneFunc 返回站点时区。
库里的时间戳一律是 UTC,进模板前必须换算成站点时区:否则东八区凌晨 零点到八点发布的内容,前台显示的日期会早一天。
type View ¶
type View struct {
Name string `json:"name"`
Label string `json:"label"`
Version string `json:"version"`
Description string `json:"description"`
Author string `json:"author"`
Homepage string `json:"homepage"`
Repo string `json:"repo"`
License string `json:"license"`
// Builtin 为真表示内置主题,不可删除。
Builtin bool `json:"builtin"`
// Source 说明内置主题的来源:embedded(二进制里那份)或 disk(data/themes 下的副本)。
// 非内置主题为空串。界面据此告诉站长「你改的文件到底算不算数」。
Source string `json:"source"`
// Active 为真表示当前启用。
Active bool `json:"active"`
// Templates 列出必需与可选模板的提供情况。
Templates []TemplateStatus `json:"templates"`
// PageTemplates 是可供独立页面选择的模板名(page-* 形态)。
PageTemplates []string `json:"pageTemplates"`
// HasCover 表示主题带了特色图。
HasCover bool `json:"hasCover"`
// SettingGroups 是主题声明的设置分组名。
SettingGroups []string `json:"settingGroups"`
}
View 是主题的接口视图。