theme

package
v0.1.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 22, 2026 License: AGPL-3.0 Imports: 44 Imported by: 0

Documentation

Overview

Package theme 提供主题系统:模板引擎、主题包加载、前台路由与主题设置。

主题是一个 zip 包(theme.yaml + settings.yaml + templates/ + static/),后台上传即可切换。 模板引擎用 html/template——主题是第三方代码,XSS 面就在主题里,而它是唯一做 上下文感知转义的引擎;模板作者体验由 Hugo 风格的 layout / partial 约定与函数库补齐。

内置默认主题「墨 Ink」编译进二进制,既是开箱即用的外观,也是所有主题的回退: 第三方主题缺失可选模板时自动回退到它的同名模板。

Index

Constants

View Source
const (
	KindIndex    = "index"
	KindPost     = "post"
	KindPage     = "page"
	KindCategory = "category"
	KindTag      = "tag"
	KindArchive  = "archive"
	KindSearch   = "search"
	KindAuthor   = "author"
	KindNotFound = "404"

	// KindFavorites 是「我的收藏」页。它由本模块渲染而不是 account——
	// 那一页上是一列文章,而按可见性取文章、补作者分类标签、算分页
	// 全都在本包里(account 不为内容提供任何接口)。
	KindFavorites = "favorites"

	// 以下五种由 account 模块注入。
	KindLogin          = "login"
	KindRegister       = "register"
	KindForgotPassword = "forgot-password"
	KindResetPassword  = "reset-password"
	KindAccount        = "account"
)

路由种类。每种路由注入固定的、有文档的上下文, 保证主题作者「照着写就能跑」。

View Source
const (
	// FileManifest 是主题元信息。
	FileManifest = "theme.yaml"
	// FileSettings 是主题设置项声明,可缺省。
	FileSettings = "settings.yaml"
	// FileScreenshot 是后台展示用截图,可缺省。
	FileScreenshot = "screenshot.png"
	// DirTemplates 是模板目录。
	DirTemplates = "templates"
	// DirStatic 是静态资源目录,可缺省。
	DirStatic = "static"
)

主题包内的固定文件名。

View Source
const (
	// SourceEmbedded 表示这份内置主题直接来自二进制(磁盘上没有副本,或那份副本坏了)。
	SourceEmbedded = "embedded"
	// SourceDisk 表示它是 data/themes/<内置主题名> 下那份副本:站长可以改,改了就生效。
	SourceDisk = "disk"
)

内置主题的两种来源。

View Source
const (
	PathPosts      = "/posts/"
	PathCategories = "/categories/"
	PathTags       = "/tags/"
	PathArchives   = "/archives/"
	PathAuthors    = "/authors/"
	PathSearch     = "/search"
	// PathFavorites 是「我的收藏」页。
	//
	// 挂在 /account 下而不是做成顶层的 /favorites:顶层单段路径会遮蔽同名的独立页面
	// (已知限制,account 的那几条固定路径就是这么来的),
	// 而两段路径与 /{slug} 的兜底完全不相交,不必再往保留字清单里添一个词。
	PathFavorites = "/account/favorites"
)

前台路径前缀,与 internal/seo、internal/menu 的约定保持一致。

View Source
const AssetsPath = "/theme-assets"

AssetsPath 是主题静态资源的挂载前缀。

View Source
const BuiltinName = "ink"

BuiltinName 是内置默认主题的标识。

View Source
const EnvDevMode = "LUMO_THEME_DEV"

EnvDevMode 开启主题开发模式:模板改动自动重载、静态资源不缓存。

View Source
const Name = "theme"

Name 是模块名,也是迁移版本表的后缀(goose_db_version_theme)。

View Source
const Permission = perm.ThemesManage

Permission 是本模块相关操作所需的权限。

Variables

View Source
var (
	// ErrInvalidPackage 表示主题包不符合规范。
	ErrInvalidPackage = errors.New("主题包不合法")
	// ErrTemplateNotFound 表示模板在主题与回退主题中都不存在。
	ErrTemplateNotFound = errors.New("模板不存在")
	// ErrNotFound 表示主题未安装。
	ErrNotFound = errors.New("主题不存在")
	// ErrAlreadyExists 表示同名主题已安装。
	ErrAlreadyExists = errors.New("同名主题已存在")
	// ErrBuiltin 表示该操作不允许作用于内置主题。
	ErrBuiltin = errors.New("内置主题不可删除")
)

错误哨兵。

Functions

func AllowedExtensions

func AllowedExtensions() []string

AllowedExtensions 返回允许的扩展名,供接口文档引用。

func ContentPath

func ContentPath(kind, slug string) string

ContentPath 返回内容的前台路径。

与 internal/seo 和 internal/menu 的约定保持一致:文章在 /posts/<slug>, 独立页面直接挂在根路径 /<slug>。

func ErrTemplateMissing

func ErrTemplateMissing(err error) bool

ErrTemplateMissing 报告错误是否为模板缺失。

func IsNotFound

func IsNotFound(err error) bool

IsNotFound 报告错误是否表示对象不存在。

func ListInstalled

func ListInstalled(root string) ([]string, error)

ListInstalled 扫描 root 下已安装的主题目录名。

只列目录名不做校验:校验要解析全部模板,而列表接口可能被频繁调用。 具体主题是否可用由 Registry 在加载时判定并记录。

func MaterializeBuiltin

func MaterializeBuiltin(root string, builtinFS fs.FS) (bool, error)

MaterializeBuiltin 把内置主题解压到 root/<BuiltinName>,已存在则不动。

为什么要落到磁盘(2026-09-15 站长定):内置主题原先只活在二进制里, 于是 data/themes 空空如也 —— 站长既看不到默认主题长什么样,也改不动它, 而「默认主题也是一份普通主题」是 WordPress 那一类系统的通行做法。

落盘之后的规矩:**以磁盘为准**(缺的模板回退到二进制里的原版),后台不可删除, 改坏了走「恢复出厂」。返回 true 表示这次真的写了文件(首次启动或刚恢复出厂)。

func OptionalTemplates

func OptionalTemplates() []string

OptionalTemplates 返回可选模板名。

func PageTemplates

func PageTemplates(names []string) []string

PageTemplates 返回主题提供的 page-*.html 模板名(WordPress 模式)。

独立页面可以在后台选择其中之一;列表由主题决定,Console 据此渲染下拉框。

func Remove

func Remove(root, name string) error

Remove 删除一个已安装的主题目录。

func RequiredTemplates

func RequiredTemplates() []string

RequiredTemplates 返回必需模板名,供接口与文档引用。

func RestoreBuiltin

func RestoreBuiltin(root string, builtinFS fs.FS) error

RestoreBuiltin 把内置主题恢复成出厂状态:删掉磁盘副本再重新解压一遍。

站长的改动会全部丢失,所以调用方必须先确认。这条路径同时是「改坏了怎么回去」的 唯一答案 —— 没有它,站长改崩一个模板就只能去翻发行包重新解压。

Types

type Adjacent

type Adjacent struct {
	// Prev 是更早发布的一篇。
	Prev *PostView
	// Next 是更晚发布的一篇。
	Next *PostView
}

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 CategoriesFinder

type CategoriesFinder struct {
	// contains filtered or unexported fields
}

CategoriesFinder 是 categories 命名空间。

func (CategoriesFinder) All

func (c CategoriesFinder) All() []TermView

All 返回平铺的分类列表,每个带文章数。

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
}

Context 是注入模板的根对象,模板里以 `.` 访问。

所有路由共用一个结构体而非每种路由一个类型:模板作者只需记住一套字段名, 且 partial 可以无差别地接收任何页面的上下文(页头页脚要用 Site 与 Menus, 而它们在每种页面上都得在)。与具体路由相关的字段按需填充,其余为零值。

func (*Context) Setting

func (c *Context) Setting(group, key string) any

Setting 读取某个设置分组的公开字段值;分组未注册或字段未公开时返回 nil。

做成方法而不是让模板写 {{ index (index .Public "account") "allowRegistration" }}: 嵌套索引在模板里读不出意图,而且少写一层就会撞上「nil map 没有这个键」的渲染错误。 缺键返回 nil 而不是报错是有意的——第三方主题引用一个本项目不存在的分组时, 该少显示一个链接,而不是让整站 500。

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 做上下文感知转义。代价是模板作者体验一般, 用 Hugo 风格的 layout / partial 约定与函数库补齐。

并发安全:解析在构造期一次完成,Render 只读。

func (*Engine) Has

func (e *Engine) Has(name string) bool

Has 报告本主题自身是否提供该页面模板(不含回退)。

func (*Engine) Lookup

func (e *Engine) Lookup(name string) (*Engine, bool)

Lookup 返回渲染 name 时实际生效的引擎:本主题有就是自己,否则找回退。

func (*Engine) Names

func (e *Engine) Names() []string

Names 返回本主题自身提供的页面模板名,已排序。

func (*Engine) Render

func (e *Engine) Render(w io.Writer, name string, data any) error

Render 渲染指定模板。

模板缺失时回退到内置默认主题的同名模板,而不是报错或渲染空白—— 分类、标签、归档、搜索、作者这五个模板对主题作者是可选的。

注意回退是**整页**回退而非按块回退:缺 category.html 时整页都由默认主题渲染, 页头页脚也跟着变。按块混合会得到一个两种设计语言拼起来的页面,比整页回退更糟。

func (*Engine) SetFallback

func (e *Engine) SetFallback(fallback *Engine)

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) Count

func (v FavoritesFinder) Count(postID int64) int

Count 返回一篇内容被收藏的次数。

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) Archives

func (f *Finder) Archives() ArchivesFinder

Archives 返回 archives 命名空间。

func (*Finder) Categories

func (f *Finder) Categories() CategoriesFinder

Categories 返回 categories 命名空间。

func (*Finder) Comments

func (f *Finder) Comments() CommentsFinder

Comments 返回 comments 命名空间。

func (*Finder) Favorites

func (f *Finder) Favorites() FavoritesFinder

Favorites 返回 favorites 命名空间。

func (*Finder) Menus

func (f *Finder) Menus() MenusFinder

Menus 返回 menus 命名空间。

func (*Finder) Posts

func (f *Finder) Posts() PostsFinder

Posts 返回 posts 命名空间,模板里写 {{ posts.Recent 5 }}。

func (*Finder) Tags

func (f *Finder) Tags() TagsFinder

Tags 返回 tags 命名空间。

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

func NewFrontend(renderer *Renderer, store *Store) *Frontend

NewFrontend 构造前台处理器。

func (*Frontend) Mount

func (f *Frontend) Mount(r chi.Router)

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 管理接口。

func NewHandler

func NewHandler(m *Module) *Handler

NewHandler 构造 Handler。

func (*Handler) Register

func (h *Handler) Register(console, public huma.API)

Register 挂载接口。

全部管理操作要求 themes:manage:主题能执行任意模板逻辑并决定整站外观, 它的权限门槛与设置同级,不对编辑开放。

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) Engine

func (l *Loaded) Engine() *Engine

Engine 返回当前的模板引擎。

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,没必要再引入第二种格式。

func Install

func Install(root string, r io.ReaderAt, size int64, overwrite bool) (*Manifest, error)

Install 把一个 zip 主题包解压安装到 root 下,返回其元信息。

安装是「先解到临时目录、校验通过再改名就位」的两段式: 半个主题留在 themes/ 下比装不上更糟——站点会在切换时渲染出残缺页面。

func Validate

func Validate(dir string) (*Manifest, error)

Validate 校验一个已安装的主题目录,供启动自检与后台重新校验使用。

type MenuItemView struct {
	Label string
	URL   string
	// Target 为 _blank 时在新窗口打开。
	Target string
	Rel    string
	// Children 是子条目。
	Children []MenuItemView
}

MenuItemView 是菜单条目在模板中的视图。

type MenusFinder struct {
	// contains filtered or unexported fields
}

MenusFinder 是 menus 命名空间。

func (m MenusFinder) Get(slug string) []MenuItemView

Get 按 slug 取菜单树,只含可见且目标存在的条目。

菜单不存在时返回空切片而不是报错:主题引用了站长还没建的菜单是常见情形, 该显示的是「没有导航」,不是一个 500 页面。

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module 是主题模块。

func From

func From(a *app.App) *Module

From 取回主题模块;未装配时返回 nil。

func New

func New() *Module

New 构造模块。

func (*Module) Frontend

func (m *Module) Frontend() *Frontend

Frontend 返回前台处理器。

func (*Module) Migrations

func (m *Module) Migrations() fs.FS

Migrations 实现 app.Migrator。

func (*Module) MountFrontend

func (m *Module) MountFrontend(r chi.Router, optional func(http.Handler) http.Handler)

MountFrontend 把访客前台路由与主题静态资源挂到根路由上。

由 serve 在全部模块注册之后调用:前台的兜底路由 /{slug} 会吞掉根路径下的 一切单段路径,必须排在 /console/、/uploads/ 与 SEO 文档之后注册。

直接接受 chi.Router 而非自定义接口:前台要用 chi.URLParam 读路径参数, 本就与 chi 的路由树绑定,抽象一层只会掩盖这个事实。

optional 是「解析会话但不强制认证」的中间件(auth.Authenticator.Optional)。 页面的登录态要靠它才进得来——unlike 三平面,根路由上没有鉴权中间件, 没有它时 routes.go 的 viewerID 恒为 0,作者点自己文章的链接会看到 404。 传 nil 表示不解析会话(测试里的纯渲染场景)。

func (*Module) Name

func (m *Module) Name() string

Name 实现 app.Module。

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

func (m *Module) Register(a *app.App) error

Register 实现 app.Module:只做装配,不访问数据库。

注意主题注册表在此构造:它要加载内置主题并解析模板,是纯文件系统操作, 与数据库无关。启用哪个主题要读库,那在 Start 里做。

func (*Module) Registry

func (m *Module) Registry() *Registry

Registry 返回主题注册表,供其他模块与测试取用。

func (*Module) Renderer

func (m *Module) Renderer() *Renderer

Renderer 返回页面渲染器。

导出给 account 模块:前台账户页与全站页面必须用同一套模板引擎、同一套回退规则与 同一份主题设置,否则账户页会在换主题时独自错位。它是本模块唯一对外开放的 渲染入口,模块之间仍然不引用彼此的内部实现。

func (*Module) RestoreBuiltin

func (m *Module) RestoreBuiltin() error

RestoreBuiltin 把内置主题恢复成出厂状态(重新解压)并立即重新加载。

站长改坏了模板时这是回到原版的唯一入口 —— 没有它,就只能去翻发行包。 调用方必须先确认:站长对内置主题的改动会全部丢失。

func (*Module) Routes

func (m *Module) Routes(r app.Router)

Routes 实现 app.RouteProvider:挂载 Console 管理接口。

前台路由不在这里:它产出 HTML 而非 JSON,也不走三平面的鉴权模型, 由 serve 直接挂在根路由上(见 MountFrontend)。

func (*Module) Start

func (m *Module) Start(ctx context.Context) error

Start 实现 app.Starter:加载已安装主题并恢复启用项。

这是模块第一次被允许访问数据库的时机。

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 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
}

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) Pinned

func (p PostsFinder) Pinned(n int) []PostView

Pinned 返回置顶文章。

func (PostsFinder) Popular

func (p PostsFinder) Popular(n int) []PostView

Popular 返回按评论数排序的热门文章(判据见 Store.PopularPosts)。

func (PostsFinder) Recent

func (p PostsFinder) Recent(n int) []PostView

Recent 返回最近发布的 n 篇文章。

func (PostsFinder) Related

func (p PostsFinder) Related(postID int64, n int) []PostView

Related 返回与给定文章共享分类或标签的其他文章。

func (PostsFinder) Total

func (p PostsFinder) Total() int

Total 返回已发布且公开的文章总数。

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) Activate

func (r *Registry) Activate(name string) error

Activate 切换启用主题。调用方负责持久化。

func (*Registry) Active

func (r *Registry) Active() *Loaded

Active 返回当前启用的主题。

启用项不可用时回退到内置主题:站长删掉正在用的主题目录后, 站点应该退回默认样式继续服务,而不是整站 500。

func (*Registry) ActiveName

func (r *Registry) ActiveName() string

ActiveName 返回当前启用的主题名。

func (*Registry) AssetsHandler

func (r *Registry) AssetsHandler() http.Handler

AssetsHandler 提供主题的 static 目录,挂在 AssetsPath 下。

路径形如 /theme-assets/<主题名>/<文件>:带上主题名而不是只服务当前启用主题, 是为了让后台预览其他主题时资源也能正确加载。

func (*Registry) Broken

func (r *Registry) Broken() map[string]string

Broken 返回加载失败的主题及其原因。

func (*Registry) DevMode

func (r *Registry) DevMode() bool

DevMode 报告是否处于开发模式。

func (*Registry) Fallback

func (r *Registry) Fallback() *Loaded

Fallback 返回内置默认主题。

func (*Registry) Get

func (r *Registry) Get(name string) (*Loaded, bool)

Get 按名称取已加载主题。

func (*Registry) List

func (r *Registry) List() []*Loaded

List 返回全部已加载主题,内置主题排在最前,其余按名称排序。

func (*Registry) Load

func (r *Registry) Load(name string) error

Load 加载(或重新加载)一个已安装主题。

加载内置主题名(data/themes/<BuiltinName>)时,结果仍然是**内置**主题: 不可删除、排在列表最前、当别的主题的回退目标,只是来源标成磁盘。 这一点必须在这里保证:「重新载入模板」走的也是这个方法, 漏掉就会被悄悄降级成一个可以卸载的普通主题。

func (*Registry) LoadInstalled

func (r *Registry) LoadInstalled() error

LoadInstalled 扫描并加载 root 下已安装的主题。

单个主题加载失败不影响其余:一个坏主题不该让整站起不来, 它会被记进 broken 并在后台列表里标出原因。

func (*Registry) Reload

func (r *Registry) Reload(name string) error

Reload 重新解析指定主题的模板,供开发模式与后台手动触发使用。

func (*Registry) Unload

func (r *Registry) Unload(name string)

Unload 从注册表移除一个主题(不删磁盘文件)。

func (*Registry) Watch

func (r *Registry) Watch(ctx context.Context)

Watch 在开发模式下轮询主题目录,检测到改动即重解析模板。

只监控当前启用的非内置主题:其余主题改了也不影响任何人看到的页面。

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 NewRenderer

func NewRenderer(opts *RendererOptions) *Renderer

NewRenderer 构造 Renderer。

func (*Renderer) Location added in v0.1.1

func (r *Renderer) Location(ctx context.Context) *time.Location

Location 返回站点设置里的时区,供 Store 把库里的 UTC 时间戳换算成本地时间。

func (*Renderer) NewContext

func (r *Renderer) NewContext(ctx context.Context, req *http.Request, kind string) (*Context, error)

NewContext 组装一个页面上下文的公共部分。

func (*Renderer) PageSize

func (r *Renderer) PageSize(ctx context.Context) int

PageSize 返回前台每页条数,取自站点设置。

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
}

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。

func (*SettingsStore) Load

func (s *SettingsStore) Load(ctx context.Context, theme string) (map[string]map[string]any, error)

Load 读取某主题全部分组的已保存值。

func (*SettingsStore) Purge

func (s *SettingsStore) Purge(ctx context.Context, theme string) error

Purge 删除某主题的全部设置值,供卸载主题时调用。

func (*SettingsStore) Save

func (s *SettingsStore) Save(ctx context.Context, theme, group string, values map[string]any) error

Save 覆盖式写入某主题某分组的值。

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 持久化「当前启用的是哪个主题」。

func NewStateStore

func NewStateStore(db *bun.DB) *StateStore

NewStateStore 构造 StateStore。

func (*StateStore) Active

func (s *StateStore) Active(ctx context.Context) (string, error)

Active 读取当前启用的主题名;从未设置过时返回空串。

func (*StateStore) SetActive

func (s *StateStore) SetActive(ctx context.Context, name string) error

SetActive 写入当前启用的主题名。

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store 提供前台渲染所需的只读查询。

全部查询都内建「已发布 + 公开」的过滤:前台不存在「忘了加 where」导致 草稿泄漏的可能——那是 CMS 最不能犯的错。私密内容对作者本人的可见性 由路由层单独处理,不走这里。

func NewStore

func NewStore(db *bun.DB) *Store

NewStore 构造 Store。

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

func (s *Store) Categories(ctx context.Context) ([]TermView, error)

Categories 返回平铺的分类列表,每个带文章数。

func (*Store) CategoryTree

func (s *Store) CategoryTree(ctx context.Context) ([]*CategoryNode, error)

CategoryTree 返回带文章数的分类树。

func (*Store) CommentTree

func (s *Store) CommentTree(ctx context.Context, postID int64) ([]*CommentNode, error)

CommentTree 返回某条内容下已通过审核的评论树。

func (*Store) CountComments

func (s *Store) CountComments(ctx context.Context, postID int64) (int, error)

CountComments 返回某条内容下已通过审核的评论数。

func (*Store) CountPosts

func (s *Store) CountPosts(ctx context.Context) (int, error)

CountPosts 返回已发布且公开的文章总数。

func (*Store) FavoritePosts

func (s *Store) FavoritePosts(ctx context.Context, userID int64, page, size int) ([]PostView, int, error)

FavoritePosts 分页返回某人收藏的内容,按收藏时间倒序。

未装配收藏模块时返回空列表而不是错误:收藏页在那种装配下仍然打得开, 只是永远是空的——这与搜索页在没有 search 模块时退回模糊匹配是同一条思路。

func (*Store) Favorites

func (s *Store) Favorites() Favoriter

Favorites 返回已注入的收藏实现;未装配时为 nil。

func (*Store) GetAuthor

func (s *Store) GetAuthor(ctx context.Context, username string) (*AuthorView, error)

GetAuthor 按用户名取作者的公开信息。

停用的账号仍可访问:作者离职不代表他写过的文章要连同署名页一起消失。

func (*Store) GetCategory

func (s *Store) GetCategory(ctx context.Context, slug string) (*taxonomy.Category, error)

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) GetTag

func (s *Store) GetTag(ctx context.Context, slug string) (*taxonomy.Tag, error)

GetTag 按 slug 取标签。

func (*Store) Menu

func (s *Store) Menu(ctx context.Context, slug string) ([]MenuItemView, error)

Menu 按 slug 取菜单树,只含可见且目标仍存在的条目。

func (*Store) PageTemplatesInUse

func (s *Store) PageTemplatesInUse(ctx context.Context) ([]string, error)

PageTemplatesInUse 返回内容表里实际用到的页面模板名,供后台校验。

func (*Store) PinnedPosts

func (s *Store) PinnedPosts(ctx context.Context, n int) ([]PostView, error)

PinnedPosts 返回置顶文章。

func (*Store) PopularPosts

func (s *Store) PopularPosts(ctx context.Context, n int) ([]PostView, error)

PopularPosts 返回按评论数排序的热门文章。// PopularPosts 返回按评论数排序的热门文章。

判据只有**已通过审核的评论数**:本站不记录浏览量,也不做「阅读数」这种要额外埋点、 又能被刷的东西。评论数是现成的、需要人工审核才能涨的,做「热门」的判据正好。 评论数相同的按发布时间倒序,保证结果稳定(同分时不能每次刷新都换一批)。

页面的评论不参与:热门是文章列表,页面(关于、联系)本就不该出现在首页模块里。

func (*Store) Posts

func (s *Store) Posts(ctx context.Context, page, size int) ([]PostView, int, error)

Posts 分页返回已发布且公开的文章。

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

func (s *Store) RecentComments(ctx context.Context, n int) ([]CommentView, error)

RecentComments 返回站点最近通过审核的评论。

func (*Store) RecentPosts

func (s *Store) RecentPosts(ctx context.Context, n int) ([]PostView, error)

RecentPosts 返回最近发布的文章。

func (*Store) RelatedPosts

func (s *Store) RelatedPosts(ctx context.Context, postID int64, n int) ([]PostView, error)

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) Tags

func (s *Store) Tags(ctx context.Context, limit int) ([]TermView, error)

Tags 返回标签列表;limit > 0 时按文章数倒序取前 limit 个,否则按名称排序取全部。

func (*Store) UseFavorites

func (s *Store) UseFavorites(favorites Favoriter)

UseFavorites 注入收藏实现。

不注入时收藏页是空的、文章页不显示收藏按钮(见 Finder.Favorites 与 routes.go)—— 收藏模块没装配就等于站点没有这个功能,而不是一个点了报错的按钮。

func (*Store) UseSearcher

func (s *Store) UseSearcher(searcher Searcher)

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) All

func (t TagsFinder) All() []TermView

All 返回全部标签,按名称排序。

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

type TimezoneFunc func(context.Context) *time.Location

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"`
	// HasScreenshot 表示主题带了截图。
	HasScreenshot bool `json:"hasScreenshot"`
	// SettingGroups 是主题声明的设置分组名。
	SettingGroups []string `json:"settingGroups"`
}

View 是主题的接口视图。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL