Help us shape the future of Go! Take the 2026 Go Developer Survey

theme

package
v1.0.0 Latest Latest
Warning

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

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

Documentation

Overview

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

主题是一个 zip 包(theme.yaml + settings.yaml + templates/ + static/),后台上传即可切换。 模板引擎用 html/template——主题是第三方代码,XSS 面就在主题里,而它是唯一做 上下文感知转义的引擎;模板作者体验由 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"

	// KindPosts 是全部文章的分页列表(/posts)。首页第一页可能摆的是模块,
	// 这里才是一篇不落的完整列表。
	KindPosts = "posts"

	// 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"
	// DirTemplates 是模板目录。
	DirTemplates = "templates"
	// DirStatic 是静态资源目录,可缺省。
	DirStatic = "static"
)

主题包内的固定文件名。

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

内置主题的两种来源。

View Source
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 的约定保持一致。

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("内置主题不可删除")
)

错误哨兵。

View Source
var CoverFiles = []string{"cover.webp", "cover.png", "cover.jpg"}

CoverFiles 是后台主题页展示的特色图(4:3,建议 1200×900),可缺省;按顺序取第一张存在的。

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 OptionalTemplates

func OptionalTemplates() []string

OptionalTemplates 返回可选模板名。

func PageTemplates

func PageTemplates(names []string) []string

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

独立页面可以在后台选择其中之一;列表由主题决定,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 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) 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
	// contains filtered or unexported fields
}

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。

func (*Context) Slot added in v0.2.0

func (c *Context) Slot(name string) template.HTML

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) 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, events app.Events) *Frontend

NewFrontend 构造前台处理器;events 为 nil 时正文不经插件过滤。

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 PluginWidget added in v0.2.0

type PluginWidget struct {
	// Label 是插件给小组件起的名字,站长没填标题时可用作标题。
	Label string
	HTML  template.HTML
}

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) 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
	// 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。

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) ListPosts added in v0.1.9

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

ListPosts 按前台列表的顺序(置顶优先)返回前 n 篇文章。

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) UseExcerptFilter added in v0.2.0

func (s *Store) UseExcerptFilter(fn func(string) string)

UseExcerptFilter 注入摘要的加工函数:摘要是从正文取的纯文字,插件的短代码在里面只是一串方括号。

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

View 是主题的接口视图。

Jump to

Keyboard shortcuts

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