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

content

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: 39 Imported by: 0

Documentation

Overview

Package content 提供文章与独立页面。

两者共用一张 posts 表,以 type 区分;发布、修订、定时发布与可见性逻辑只写一份, 接口按类型分别挂在 /posts 与 /pages 下,权限分别走 posts:* 与 pages:*。 依赖 taxonomy 模块(分类与标签关联),装配时必须排在它之后。

Index

Constants

View Source
const ExcerptLength = 200

ExcerptLength 是自动摘要的最大字符数(按 Unicode 字符计)。

View Source
const Name = "content"

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

View Source
const PostsPrefix = hooks.PostsPrefix

PostsPrefix 是文章详情页的路径前缀;页面直接挂在站点根下。

View Source
const RevisionsKept = 30

RevisionsKept 是每篇内容保留的修订数上限,超出后最旧的被清理。

Variables

View Source
var (
	// ErrNotFound 表示内容或修订不存在。
	ErrNotFound = errors.New("对象不存在")
	// ErrSlugTaken 表示同类型下 slug 已被占用。
	ErrSlugTaken = errors.New("slug 已被占用")
	// ErrTermNotFound 表示引用的分类或标签不存在。
	ErrTermNotFound = errors.New("分类或标签不存在")
	// ErrNotTrashed 表示内容不在回收站,无法彻底删除。
	ErrNotTrashed = errors.New("内容不在回收站")
	// ErrInvalid 表示字段违反了数据库约束。
	ErrInvalid = errors.New("字段不合法")
)

错误哨兵。处理器据此映射为 400 / 404 / 409。

View Source
var ErrInvalidType = errors.New("内容类型只能是 post 或 page")

ErrInvalidType 表示内容类型不是 post 或 page。

Functions

func Excerpt

func Excerpt(content string, limit int) string

Excerpt 从渲染后的 HTML 生成纯文本摘要:去标签、解实体、折叠空白,超长时截断并加省略号。

func Highlight

func Highlight(rendered string) string

Highlight 给渲染后的 HTML 里的代码块加上语法高亮与结构外壳。

只改写 <pre>…</pre> 这一段,其余字节原样保留——正文里其他部分不该因为 「顺手过了一遍 HTML 解析器」而被重新序列化。

func HookPost added in v0.2.0

func HookPost(p *Post) hooks.Post

HookPost 把内容转成派给插件的动作数据。

func NewPolicy added in v0.2.0

func NewPolicy() *bluemonday.Policy

NewPolicy 返回一份新的正文允许列表策略,供要在它之上再放行几样东西的调用方扩展(插件的前台片段)。

func Path added in v0.2.0

func Path(t Type, slug string) string

Path 返回一条内容在前台的路径。

func Render

func Render(rawType RawType, raw string) (string, error)

Render 把原稿渲染为主题消费的 HTML。

HTML 原稿原样返回:编辑器产出的就是规范 HTML;Markdown 经 goldmark 渲染, 标题锚点用 slug 规则生成,中文标题得到可读的中文 ID。

func Sanitize

func Sanitize(html string) string

Sanitize 按允许列表净化 HTML。仅用于没有 content:unsafe_html 权限的作者产出的正文。

func StripTags

func StripTags(s string) string

StripTags 去掉 HTML 标签(含 script 与 style 的内容),解码实体并折叠空白。

每个标签边界都产生一个空格,"<p>a</p><p>b</p>" 得到 "a b" 而不是 "ab"。 这不是净化器,只用于生成摘要与搜索文本。

Types

type AuthorView

type AuthorView struct {
	ID          int64  `json:"id"`
	Username    string `json:"username"`
	DisplayName string `json:"displayName"`
	AvatarURL   string `json:"avatarUrl"`
}

AuthorView 是嵌入内容响应的作者摘要,只含可公开的字段。

type Filter

type Filter struct {
	Type Type
	// Status 为空表示回收站之外的全部状态。
	Status Status
	// AuthorID 非 0 时只取该作者的内容。
	AuthorID int64
	// CategorySlug / TagSlug 非空时按分类或标签筛选。
	CategorySlug string
	TagSlug      string
	// Query 非空时按标题模糊匹配。
	Query string
	// PublicOnly 为真时只取已发布内容:公开的,或属于 ViewerID 的私密内容。
	PublicOnly bool
	ViewerID   int64
}

Filter 是列表筛选条件。

type Handler

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

Handler 提供内容的 Console 与 Public 接口。

func NewHandler

func NewHandler(store *Store, slugify Slugger, events app.Events) *Handler

NewHandler 构造 Handler;slugify 为 nil 时使用保留中文的缺省策略,events 为 nil 时不派发动作。

func (*Handler) Register

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

Register 为文章与页面各挂一套接口。

type Module

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

Module 是内容模块。

func From added in v0.2.0

func From(a *app.App) *Module

From 取回内容模块;未装配时返回 nil。

func New

func New() *Module

New 构造模块。

func (*Module) Migrations

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

Migrations 实现 app.Migrator。

func (*Module) Name

func (m *Module) Name() string

Name 实现 app.Module。

func (*Module) Navigation

func (m *Module) Navigation() app.Navigation

Navigation 实现 app.NavigationProvider。

func (*Module) Permissions

func (m *Module) Permissions() []app.Permission

Permissions 实现 app.PermissionProvider。

func (*Module) PublishDue

func (m *Module) PublishDue(ctx context.Context) (int64, error)

PublishDue 立即执行一次定时发布扫描,返回推进条数;供后台任务与测试调用。

func (*Module) Register

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

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

若 settings 模块先于本模块装配,slug 生成策略跟随站点设置;否则退回保留中文的缺省策略。

func (*Module) Routes

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

Routes 实现 app.RouteProvider。

func (*Module) Start

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

Start 实现 app.Starter:启动定时发布扫描,ctx 取消时退出。

扫描是幂等的单条 UPDATE,多实例部署时各实例都跑也不会重复发布。

func (*Module) Writer added in v0.2.0

func (m *Module) Writer() *Writer

Writer 返回给插件这类内部调用方用的写入口;没有数据库时为 nil。

type Post

type Post struct {
	bun.BaseModel `bun:"table:posts,alias:p"`

	ID          int64          `bun:"id,pk,autoincrement" json:"id"`
	Type        Type           `bun:"type,notnull"        json:"type" enum:"post,page"`
	Title       string         `bun:"title,notnull"       json:"title"`
	Slug        string         `bun:"slug,notnull"        json:"slug" doc:"URL 片段,同类型内唯一"`
	Status      Status         `bun:"status,notnull"      json:"status" enum:"draft,published,scheduled,trashed"`
	Visibility  Visibility     `bun:"visibility,notnull"  json:"visibility" enum:"public,private"`
	RawType     RawType        `bun:"raw_type,notnull"    json:"rawType" enum:"html,markdown" doc:"原稿格式"`
	Raw         string         `bun:"raw"                 json:"raw" doc:"原稿:Markdown 源码或规范 HTML"`
	Content     string         `bun:"content"             json:"content" doc:"渲染后的 HTML,主题只消费此字段"`
	Excerpt     string         `bun:"excerpt"             json:"excerpt"`
	ExcerptAuto bool           `bun:"excerpt_auto"        json:"excerptAuto" doc:"摘要是否由正文自动生成"`
	CoverURL    string         `bun:"cover_url"           json:"coverUrl"`
	Pinned      bool           `bun:"pinned"              json:"pinned"`
	Template    string         `bun:"template"            json:"template" doc:"页面模板名,如 page-about;空为默认"`
	AuthorID    int64          `bun:"author_id,notnull"   json:"authorId"`
	PublishedAt *time.Time     `bun:"published_at"        json:"publishedAt" doc:"发布时间;定时发布时为计划时间"`
	TrashedAt   *time.Time     `bun:"trashed_at"          json:"trashedAt"`
	Meta        map[string]any `bun:"meta,type:jsonb"     json:"meta" doc:"扩展元数据"`
	CreatedAt   time.Time      `bun:"created_at,nullzero" json:"createdAt"`
	UpdatedAt   time.Time      `bun:"updated_at,nullzero" json:"updatedAt"`

	// 以下为关联数据,由存储层按需填充,不是数据库列。
	Author     *AuthorView         `bun:"-" json:"author,omitempty"`
	Categories []taxonomy.Category `bun:"-" json:"categories"`
	Tags       []taxonomy.Tag      `bun:"-" json:"tags"`
}

Post 是文章或页面实体。

func (*Post) IsPublic

func (p *Post) IsPublic() bool

IsPublic 报告内容是否对匿名访客可见。

type PostCategory

type PostCategory struct {
	bun.BaseModel `bun:"table:post_categories,alias:pc"`

	PostID     int64 `bun:"post_id,pk"`
	CategoryID int64 `bun:"category_id,pk"`
}

PostCategory 是文章与分类的关联。

type PostTag

type PostTag struct {
	bun.BaseModel `bun:"table:post_tags,alias:pt"`

	PostID int64 `bun:"post_id,pk"`
	TagID  int64 `bun:"tag_id,pk"`
}

PostTag 是文章与标签的关联。

type RawType

type RawType string

RawType 是原稿格式。

const (
	RawHTML     RawType = "html"
	RawMarkdown RawType = "markdown"
)

原稿格式。

type Revision

type Revision struct {
	bun.BaseModel `bun:"table:post_revisions,alias:rv"`

	ID        int64     `bun:"id,pk,autoincrement" json:"id"`
	PostID    int64     `bun:"post_id,notnull"     json:"postId"`
	AuthorID  *int64    `bun:"author_id"           json:"authorId" doc:"保存该版本的用户;用户已删除时为 null"`
	Title     string    `bun:"title,notnull"       json:"title"`
	RawType   RawType   `bun:"raw_type,notnull"    json:"rawType" enum:"html,markdown"`
	Raw       string    `bun:"raw"                 json:"raw"`
	Content   string    `bun:"content"             json:"content"`
	Excerpt   string    `bun:"excerpt"             json:"excerpt"`
	CreatedAt time.Time `bun:"created_at,nullzero" json:"createdAt"`
}

Revision 是一次内容快照。

type RevisionSummary

type RevisionSummary struct {
	bun.BaseModel `bun:"table:post_revisions,alias:rv"`

	ID        int64     `bun:"id,pk"            json:"id"`
	PostID    int64     `bun:"post_id"          json:"postId"`
	AuthorID  *int64    `bun:"author_id"        json:"authorId"`
	Title     string    `bun:"title"            json:"title"`
	RawType   RawType   `bun:"raw_type"         json:"rawType" enum:"html,markdown"`
	CreatedAt time.Time `bun:"created_at,nullzero" json:"createdAt"`
}

RevisionSummary 是修订列表项,不含正文以免列表过大。

type Slugger

type Slugger func(ctx context.Context, text string) string

Slugger 由文本生成 slug;站点设置决定保留中文还是转拼音。

type Status

type Status string

Status 是内容的生命周期状态。

const (
	// StatusDraft 是草稿:仅作者与有权限者可见。
	StatusDraft Status = "draft"
	// StatusPublished 已发布。
	StatusPublished Status = "published"
	// StatusScheduled 定时发布:到 PublishedAt 时由后台任务推进为已发布。
	StatusScheduled Status = "scheduled"
	// StatusTrashed 在回收站:可恢复,也可彻底删除。
	StatusTrashed Status = "trashed"
)

内容状态。

type Store

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

Store 提供内容的持久化操作。

func NewStore

func NewStore(db *bun.DB, users *auth.Store) *Store

NewStore 构造 Store。

func (*Store) Create

func (s *Store) Create(ctx context.Context, p *Post, categoryIDs, tagIDs []int64) error

Create 创建内容、写入分类与标签关联,并记录首个修订;成功后回填 ID、时间戳与关联。

func (*Store) DeletePermanently

func (s *Store) DeletePermanently(ctx context.Context, typ Type, id int64) error

DeletePermanently 彻底删除内容,关联与修订随外键级联消失。

删除条件里带上 status = 'trashed',让「检查是否在回收站」与「删除」成为同一条语句: 先读后删之间存在窗口,用户在这期间点了「恢复」,刚恢复的内容就会被永久删除, 连同它的修订与评论一起不可逆地消失。条件不满足时返回 ErrNotTrashed。

func (*Store) Get

func (s *Store) Get(ctx context.Context, typ Type, id int64) (*Post, error)

Get 按 ID 取内容(含关联)。

func (*Store) GetBySlug

func (s *Store) GetBySlug(ctx context.Context, typ Type, slug string) (*Post, error)

GetBySlug 按 slug 取内容(含关联)。

func (*Store) Page

func (s *Store) Page(ctx context.Context, f *Filter, params api.PageParams) ([]Post, int, error)

Page 分页返回内容并附上作者、分类与标签。

前台按置顶与发布时间排序,后台按更新时间排序——编辑者关心的是最近动过的内容。

func (*Store) PublishDue

func (s *Store) PublishDue(ctx context.Context) ([]Post, error)

PublishDue 把计划时间已到的定时内容推进为已发布,返回被推进的那些。

单条幂等 UPDATE:多实例同时执行也不会重复发布或互相干扰;只有真正改到行的那个实例 拿得到 RETURNING 的结果,post.published 因此只发一次。

func (*Store) Revision

func (s *Store) Revision(ctx context.Context, postID, revisionID int64) (*Revision, error)

Revision 取指定修订的完整内容。

func (*Store) Revisions

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

Revisions 返回内容的修订列表,新的在前。

func (*Store) Update

func (s *Store) Update(ctx context.Context, p *Post, categoryIDs, tagIDs []int64, snapshot bool, editorID int64) error

Update 更新内容的可编辑字段与关联;snapshot 为真时追加修订并清理超出上限的旧修订。

func (*Store) UpdateStatus

func (s *Store) UpdateStatus(ctx context.Context, p *Post) error

UpdateStatus 只写入状态相关列:status、published_at、trashed_at 与 updated_at。

type Type

type Type string

Type 区分文章与独立页面,两者共用一张表。

const (
	TypePost Type = "post"
	TypePage Type = "page"
)

内容类型。

type Visibility

type Visibility string

Visibility 是内容对前台的可见性。

const (
	VisibilityPublic  Visibility = "public"
	VisibilityPrivate Visibility = "private"
)

可见性。

type WriteParams added in v0.2.0

type WriteParams struct {
	Title string `json:"title"`
	// Slug 留空时新建按站点策略由标题生成、修改保留原值。
	Slug string `json:"slug"`
	// RawType 是原稿格式,缺省 html。
	RawType     RawType `json:"rawType"`
	Raw         string  `json:"raw"`
	Excerpt     string  `json:"excerpt"`
	CategoryIDs []int64 `json:"categoryIds"`
	TagIDs      []int64 `json:"tagIds"`
}

WriteParams 是新建或修改一条内容的字段。

type Writer added in v0.2.0

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

Writer 是给插件这类内部调用方用的写入口。

规则与后台接口一致:正文渲染与净化(一律按没有 content:unsafe_html 的作者处理)、 slug 生成与去重、动作派发都走同一套代码。权限由调用方判定——这里不认识调用方是谁。

func (*Writer) Create added in v0.2.0

func (w *Writer) Create(ctx context.Context, typ Type, authorID int64, p *WriteParams, publish bool) (*Post, error)

Create 新建一条内容,作者为 authorID;publish 为真时直接发布,否则存为草稿。

func (*Writer) Trash added in v0.2.0

func (w *Writer) Trash(ctx context.Context, typ Type, id int64) error

Trash 把一条内容移入回收站。

func (*Writer) Update added in v0.2.0

func (w *Writer) Update(ctx context.Context, typ Type, id int64, p *WriteParams, editorID int64) (*Post, error)

Update 整体改写一条内容的标题与正文,不改变它的发布状态。editorID 记进修订历史。

Jump to

Keyboard shortcuts

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