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

content

package
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: Oct 20, 2026 License: AGPL-3.0 Imports: 40 Imported by: 0

Documentation

Overview

Package content manages content types, custom fields, content items, revisions and queries.

contract.go holds the frozen wave-0 contract (types and interfaces only). The implementation arrives in wave 1.

Index

Constants

View Source
const (
	CommentPending  = "pending"
	CommentApproved = "approved"
	CommentSpam     = "spam"
	CommentTrash    = "trash"
)

Comment statuses.

View Source
const PermModerateComments = "comments.moderate"

PermModerateComments lets a user approve, mark as spam, trash and delete comments; their own comments are approved at once.

View Source
const ServiceComments = "content.comments"

ServiceComments is the kernel service name of the CommentService.

Variables

View Source
var (
	// ErrForbidden: the acting user may not perform the action (e.g. an
	// author editing someone else's item, or publishing without
	// content.publish).
	ErrForbidden = errors.New("content: forbidden")
	// ErrSystemType: system types (post, page) cannot be deleted.
	ErrSystemType = errors.New("content: system type")
	// ErrTypeInUse: a type with content cannot be deleted.
	ErrTypeInUse = errors.New("content: type has content")
)

CommentStatuses are the comment statuses, in moderation order.

View Source
var ErrConflict = errors.New("content: changed since it was loaded")
View Source
var ErrNotFound = errors.New("content: not found")
View Source
var ErrPublishConflict = errors.New("content: reviewed content has changed")

FieldTypes lists every field type in builder order.

View Source
var Statuses = []Status{"draft", "pending", "scheduled", "published", "private", "trash"}

Statuses lists every item status.

View Source
var Supports = []string{"title", "body", "excerpt", "featured_image", "author", "revisions", "seo", "parent", "menu_order", "comments"}

Supports lists the editor features a type may enable.

Functions

func DirectPageReadAllowed

func DirectPageReadAllowed(ctx context.Context, id string) bool

func EditToken

func EditToken(it *Item) string

EditToken is an opaque optimistic version of persisted content. Unlike second-resolution timestamps, it detects two edits within the same second.

func EditTokenInTx

func EditTokenInTx(ctx context.Context, tx *sql.Tx, id string) (string, error)

EditTokenInTx reads the same persisted snapshot while the caller holds a write transaction, so a first draft save cannot accept a stale content base.

func GuardUnapprovedMutation

func GuardUnapprovedMutation(ctx context.Context) context.Context

GuardUnapprovedMutation restricts a caller to unchanged, non-live content at commit time. Only trusted server code can attach this context value; request JSON cannot authorize a live mutation. Normal API/editor saves are unaffected.

func ParseDatetime

func ParseDatetime(v string) (time.Time, bool)

ParseDatetime parses RFC3339 or the HTML datetime-local forms ("2006-01-02T15:04[:05]", "2006-01-02 15:04[:05]", taken as UTC).

func Policy

func Policy() *bluemonday.Policy

Policy returns the "cms" sanitizer policy (spec §4.2): bluemonday's UGC base (headings, lists, tables, links, images, code) plus figure/figcaption, details/summary, iframes from an allowlist, editor classes and dir. data-* attributes, inline styles, scripts and event handlers are stripped.

func PubliclyReadable

func PubliclyReadable(ctx context.Context, svc Service, id string) bool

PubliclyReadable is separate from staff read authorization. A visitor's unlock grants the direct page, never searchable excerpts or shared caches.

func Sanitize

func Sanitize(html string) string

Sanitize cleans HTML with the "cms" policy, then pins every iframe's sandbox to iframeSandbox (bluemonday can filter sandbox values but not set them, and a bare sandbox="" would break video players).

func Slugify

func Slugify(s string, asciiOnly bool) string

Slugify builds a URL slug: lowercase, spaces/underscores/dashes collapsed to single "-", and only letters and digits of any script kept, so Arabic stays UTF-8 ("مرحبا بالعالم" → "مرحبا-بالعالم"); marks, punctuation and bidi controls are dropped. With asciiOnly (setting content.transliterate_slugs) accented Latin letters are folded to ASCII and other non-ASCII letters dropped.

func WithDirectPageRead

func WithDirectPageRead(ctx context.Context, id string) context.Context

func WithPublicExclusions

func WithPublicExclusions(ctx context.Context, ids []string) context.Context

Types

type AtomicUpdater

type AtomicUpdater interface {
	UpdateAtomic(context.Context, string, Input, string, func(*sql.Tx, *Item) error) (*Item, error)
}

AtomicUpdater joins a module's dependent write to a validated content save. Taxonomy assignments must remain unchanged; regular Update handles taxonomy edits. The callback must use tx rather than the database pool.

type BatchPublisher

type BatchPublisher interface {
	PublishBatch(context.Context, []PublishExpectation, func(*sql.Tx, []*Item) error) error
}

BatchPublisher publishes a reviewed collection and companion settings atomically. The companion callback must use tx and must not call a pooled service method.

type Comment

type Comment struct {
	ID           string `json:"id"`
	ContentID    string `json:"content_id"`
	ContentTitle string `json:"content_title,omitempty"` // filled by Comments
	ContentType  string `json:"content_type,omitempty"`  // filled by Comments
	ParentID     string `json:"parent_id"`
	AuthorName   string `json:"author_name"`
	AuthorEmail  string `json:"author_email"`
	UserID       string `json:"user_id"`
	Body         string `json:"body"`
	Status       string `json:"status"`
	CreatedAt    string `json:"created_at"`
	UpdatedAt    string `json:"updated_at"`
}

Comment is a visitor comment. AuthorEmail is for moderators only: never render it publicly.

type CommentInput

type CommentInput struct {
	ContentID, ParentID, AuthorName, AuthorEmail, Body string
}

CommentInput is a submitted comment.

type CommentQuery

type CommentQuery struct {
	ContentID string `json:"content_id"`
	Status    string `json:"status"`
	Page      int    `json:"page"`
	PerPage   int    `json:"per_page"`
}

CommentQuery filters the moderation list (newest first).

type CommentService

type CommentService interface {
	// SubmitComment stores a comment on a published item of a public type that supports
	// comments: pending, or approved when the user holds comments.moderate.
	SubmitComment(ctx context.Context, in CommentInput) (*Comment, error)
	// ApprovedComments lists the approved comments of an item, oldest first, at most limit.
	ApprovedComments(ctx context.Context, contentID string, limit int) ([]Comment, error)
	Comments(ctx context.Context, q CommentQuery) (Page[Comment], error)
	GetComment(ctx context.Context, id string) (*Comment, error)
	SetCommentStatus(ctx context.Context, id, status string) error
	DeleteComment(ctx context.Context, id string) error
	CommentCounts(ctx context.Context) (map[string]int, error)
}

CommentService is the comments API of the content module.

type CreateInput

type CreateInput struct {
	Type            string                `json:"type" doc:"An existing content type slug is required."`
	Slug            *string               `json:"slug,omitempty" doc:"Omitted, null or empty generates a slug from the title."`
	Title           *string               `json:"title,omitempty" doc:"Optional title; defaults to empty."`
	Excerpt         *string               `json:"excerpt,omitempty" doc:"Optional excerpt; defaults to empty."`
	Body            *string               `json:"body,omitempty" doc:"Optional sanitized HTML; defaults to empty."`
	Fields          *map[string]any       `json:"fields,omitempty" doc:"Custom fields are validated against the content type, including defaults and required fields."`
	AuthorID        *string               `json:"author_id,omitempty" doc:"Defaults to the acting user; ownership permissions apply."`
	Status          *Status               `json:"status,omitempty" doc:"Defaults to draft. Publishing requires content.publish; future publication becomes scheduled."`
	ParentID        *string               `json:"parent_id,omitempty" doc:"Optional parent ID; defaults to empty."`
	MenuOrder       *int                  `json:"menu_order,omitempty" doc:"Defaults to zero."`
	Template        *string               `json:"template,omitempty" doc:"Optional rendering template; defaults to empty."`
	FeaturedMediaID *string               `json:"featured_media_id,omitempty" doc:"Optional featured media ID; defaults to empty."`
	SEO             *map[string]any       `json:"seo,omitempty" doc:"Optional SEO object; defaults to empty."`
	Locale          *string               `` /* 126-byte string literal not displayed */
	PublishAt       *string               `json:"publish_at,omitempty" format:"-" doc:"Optional supported datetime; required for scheduled status."`
	Terms           *map[string]*[]string `json:"terms,omitempty" doc:"Optional taxonomy-to-slug arrays; defaults to empty."`
}

CreateInput documents creation's required type and optional defaulted state.

type DraftCreator added in v0.5.0

type DraftCreator interface {
	CreateDraftWith(context.Context, Input, func(*sql.Tx, *Item) error) (*Item, error)
}

DraftCreator atomically saves a new draft and its caller-owned provenance. The callback participates in the same transaction as content, revisions and audit. It must not perform network calls or start another database transaction.

type Field

type Field struct {
	Key        string            `json:"key"`
	Label      map[string]string `json:"label"`
	Type       FieldType         `json:"type"`
	Required   bool              `json:"required"`
	Indexed    bool              `json:"indexed"`
	Multiple   bool              `json:"multiple"`
	Default    any               `json:"default"`
	Help       map[string]string `json:"help,omitempty"`
	Options    []Option          `json:"options,omitempty"`
	Min        *float64          `json:"min,omitempty"`
	Max        *float64          `json:"max,omitempty"`
	Step       *float64          `json:"step,omitempty"`
	SubFields  []Field           `json:"sub_fields,omitempty"`
	TargetType string            `json:"target_type,omitempty"`
	Mime       []string          `json:"mime,omitempty"`
}

type FieldErrors

type FieldErrors map[string]string

func ValidateTypeSchema

func ValidateTypeSchema(t *Type) FieldErrors

ValidateTypeSchema checks a type definition: slug format, a name, valid supports, and every field (snake_case unique keys, known types, indexed only on scalars, sub_fields only on repeaters, target_type only — and always — on relationships, options on select/multiselect). Error keys are "slug", "name", "supports", "fields.<i>.<attr>".

type FieldType

type FieldType string // text textarea richtext number date datetime boolean select multiselect repeater relationship image file json
const (
	FieldText         FieldType = "text"
	FieldTextarea     FieldType = "textarea"
	FieldRichtext     FieldType = "richtext"
	FieldNumber       FieldType = "number"
	FieldDate         FieldType = "date"
	FieldDatetime     FieldType = "datetime"
	FieldBoolean      FieldType = "boolean"
	FieldSelect       FieldType = "select"
	FieldMultiselect  FieldType = "multiselect"
	FieldRepeater     FieldType = "repeater"
	FieldRelationship FieldType = "relationship"
	FieldImage        FieldType = "image"
	FieldFile         FieldType = "file"
	FieldJSON         FieldType = "json"
)

Field types (spec §3).

func (FieldType) IsScalar

func (ft FieldType) IsScalar() bool

IsScalar reports whether values of ft can be indexed (spec: text, number, date, datetime, boolean, select).

type Filter

type Filter struct {
	Op    string `json:"op"`
	Value any    `json:"value"`

} // eq ne gt lt gte lte in like

type Input

type Input struct {
	Type            string              `json:"type"`
	Slug            string              `json:"slug"`
	Title           string              `json:"title"`
	Excerpt         string              `json:"excerpt"`
	Body            string              `json:"body"`
	Fields          map[string]any      `json:"fields"`
	AuthorID        string              `json:"author_id"`
	Status          Status              `json:"status"`
	ParentID        string              `json:"parent_id"`
	MenuOrder       *int                `json:"menu_order"`
	Template        string              `json:"template"`
	FeaturedMediaID string              `json:"featured_media_id"`
	SEO             map[string]any      `json:"seo"`
	Locale          string              `json:"locale"`
	PublishAt       string              `json:"publish_at"`
	Terms           map[string][]string `json:"terms"`
}

Input is the writable state of an item. Create builds an item from it; Update REPLACES the item's writable state with it in full (zero values clear fields). To change some fields, load the item, convert it with InputFromItem, modify, then call Update. MenuOrder nil means 0.

func InputFromItem

func InputFromItem(it *Item) Input

InputFromItem returns the Input that reproduces it (all writable fields, including Terms), for load-modify-Update.

type Item

type Item struct {
	ID              string              `json:"id"`
	Type            string              `json:"type"`
	Slug            string              `json:"slug"`
	Title           string              `json:"title"`
	Excerpt         string              `json:"excerpt"`
	Body            string              `json:"body"`
	BodyFormat      string              `json:"body_format"`
	Fields          map[string]any      `json:"fields"`
	AuthorID        string              `json:"author_id"`
	Status          Status              `json:"status"`
	ParentID        string              `json:"parent_id"`
	MenuOrder       int                 `json:"menu_order"`
	Template        string              `json:"template"`
	FeaturedMediaID string              `json:"featured_media_id"`
	SEO             map[string]any      `json:"seo"`
	Locale          string              `json:"locale"`
	PublishAt       string              `json:"publish_at"`
	PublishedAt     string              `json:"published_at"`
	CreatedAt       string              `json:"created_at"`
	UpdatedAt       string              `json:"updated_at"`
	Terms           map[string][]string `json:"terms"` // taxonomy → term slugs (filled by Query/Get)
	// TranslationGroup links the item to its translations ("" = none); changed only through the
	// translation operations, never by Create or Update.
	TranslationGroup string `json:"translation_group"`
}

type Module

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

Module is the content kernel.Module.

func New

func New() *Module

New returns the content module.

func (*Module) Catalogs

func (*Module) Catalogs() fs.FS

Catalogs implements kernel.CatalogProvider.

func (*Module) Migrations

func (*Module) Migrations() fs.FS

Migrations implements kernel.Module.

func (*Module) Name

func (*Module) Name() string

Name implements kernel.Module.

func (*Module) Register

func (m *Module) Register(d *kernel.Deps) error

Register publishes content.Service (kernel.ServiceContent), declares permissions, schedules the publish/purge crons and mounts the admin screens, API operations and the dashboard widget.

type Option

type Option struct {
	Value string            `json:"value"`
	Label map[string]string `json:"label"`
}

type Order

type Order struct {
	By  string `json:"by"`
	Dir string `json:"dir"`
}

type Page

type Page[T any] struct {
	Items   []T `json:"items"`
	Total   int `json:"total"`
	Page    int `json:"page"`
	PerPage int `json:"per_page"`
}

type PageAccess

type PageAccess struct {
	ContentID    string `json:"content_id"`
	Mode         string `json:"mode"`
	Message      string `json:"message"`
	Revision     int64  `json:"revision"`
	PasswordHash string `json:"-"`
}

type PageAccessInput

type PageAccessInput struct {
	Mode, Message, Password string
	ClearPassword           bool
}

type PageAccessService

type PageAccessService interface {
	PageAccess(context.Context, string) (PageAccess, error)
	SavePageAccess(context.Context, string, PageAccessInput, int64) (PageAccess, error)
}

type PatchInput

type PatchInput struct {
	Type            *string               `json:"type,omitempty" doc:"Omitted or null keeps the type. A different type is rejected; empty keeps the existing type."`
	Slug            *string               `json:"slug,omitempty" doc:"Omitted or null keeps the slug; empty regenerates it from the title."`
	Title           *string               `json:"title,omitempty" doc:"Omitted or null keeps the title; empty clears it."`
	Excerpt         *string               `json:"excerpt,omitempty" doc:"Omitted or null keeps the excerpt; empty clears it."`
	Body            *string               `json:"body,omitempty" doc:"Omitted or null keeps the body; empty clears it. HTML is sanitized."`
	Fields          *map[string]any       `` /* 214-byte string literal not displayed */
	AuthorID        *string               `` /* 142-byte string literal not displayed */
	Status          *Status               `json:"status,omitempty" doc:"Omitted or null keeps status; empty resets status to draft. Use DELETE to trash."`
	ParentID        *string               `json:"parent_id,omitempty" doc:"Omitted or null keeps the parent; empty clears it."`
	MenuOrder       *int                  `json:"menu_order,omitempty" doc:"Omitted keeps order; null resets it to zero."`
	Template        *string               `json:"template,omitempty" doc:"Omitted or null keeps the template; empty clears it."`
	FeaturedMediaID *string               `json:"featured_media_id,omitempty" doc:"Omitted or null keeps featured media; empty clears it."`
	SEO             *map[string]any       `json:"seo,omitempty" doc:"Omitted keeps SEO; an object merges by key (empty object keeps values); null clears all SEO."`
	Locale          *string               `json:"locale,omitempty" doc:"Omitted, null or empty keeps the existing locale."`
	PublishAt       *string               `` /* 180-byte string literal not displayed */
	Terms           *map[string]*[]string `` /* 180-byte string literal not displayed */
}

PatchInput documents the merge decoder in patchContent; it is deliberately separate from Input, whose service-level Update contract replaces all fields.

type PublishExpectation

type PublishExpectation struct {
	ID, UpdatedAt string
	Body          *string
	Template      string
}

PublishExpectation guards reviewed content against changes before activation.

type Query

type Query struct {
	Type         string              `json:"type"`
	Status       string              `json:"status"`
	Search       string              `json:"search"`
	AuthorID     string              `json:"author_id"`
	ParentID     string              `json:"parent_id"`
	Locale       string              `json:"locale"`
	Taxonomy     map[string][]string `json:"taxonomy"`
	Fields       map[string]Filter   `json:"fields"`
	Order        []Order             `json:"order"`
	Page         int                 `json:"page"`
	PerPage      int                 `json:"per_page"`
	IncludeTrash bool                `json:"include_trash"`
	// PublicLocale keeps the items of one public-site locale plus those stored without a locale
	// (the site's own listings); it is not part of the REST or MCP query.
	PublicLocale string `json:"-"`
	// Template selects an exact rendering template for native administrative pickers.
	Template string `json:"-"`
	// PublicOnly excludes internal content types and protected pages before counting and pagination.
	PublicOnly bool `json:"-"`
}

func QueryFromValues

func QueryFromValues(v url.Values) Query

QueryFromValues parses the list query string: type, status, search, author_id, parent_id, locale, page, per_page, order (by[:dir],...), taxonomy.<tax>=a,b and fields.<key>.<op>=value (in: comma-separated).

type Revision

type Revision struct {
	ID        string `json:"id"`
	ContentID string `json:"content_id"`
	AuthorID  string `json:"author_id"`
	CreatedAt string `json:"created_at"`
	Snapshot  Item   `json:"snapshot"`
}

type SaveAuthorization

type SaveAuthorization struct {
	Tx   *sql.Tx
	Item *Item
}

SaveAuthorization is an extension's transaction-scoped authorization input. Hooks must use Tx for database reads and must not commit or retain it. This hook supplements (never replaces) native ownership and permissions.

type Service

type Service interface {
	Types(ctx context.Context) ([]Type, error)
	GetType(ctx context.Context, slug string) (*Type, error)
	SaveType(ctx context.Context, t Type) error
	DeleteType(ctx context.Context, slug string) error
	Query(ctx context.Context, q Query) (Page[Item], error)
	Get(ctx context.Context, id string) (*Item, error)
	GetBySlug(ctx context.Context, typ, slug, locale string) (*Item, error)
	Create(ctx context.Context, in Input) (*Item, error)
	Update(ctx context.Context, id string, in Input) (*Item, error) // full replace; see Input
	Delete(ctx context.Context, id string, hard bool) error
	Publish(ctx context.Context, id string) error
	Unpublish(ctx context.Context, id string) error
	Revisions(ctx context.Context, id string) ([]Revision, error)
	Restore(ctx context.Context, id, revisionID string) (*Item, error)
	Validate(t *Type, fields map[string]any) (map[string]any, FieldErrors)
	Reindex(ctx context.Context) error
	// Translations returns the item and the items linked to it as translations, by locale.
	Translations(ctx context.Context, id string) ([]Item, error)
	// CreateTranslation copies the item into locale as a linked draft.
	CreateTranslation(ctx context.Context, id, locale string) (*Item, error)
	// LinkTranslation links other (and its translations) to the item's translations.
	LinkTranslation(ctx context.Context, id, other string) error
	// LinkTranslationGuarded atomically requires the exact current union of member IDs.
	LinkTranslationGuarded(ctx context.Context, id, other string, expectedIDs []string) error
	// UnlinkTranslation takes the item out of its translations.
	UnlinkTranslation(ctx context.Context, id string) error
}

type Status

type Status string // draft pending scheduled published private trash

type TermBackend

type TermBackend interface {
	SetContentTerms(ctx context.Context, contentID string, terms map[string][]string) error
	ContentTerms(ctx context.Context, contentID string) (map[string][]string, error)
	ContentIDs(ctx context.Context, taxonomy string, termSlugs []string) ([]string, error)
}

TermBackend is the slice of taxonomy.Service content needs. content boots before taxonomy and may not import it, so the backend is looked up lazily (kernel.ServiceTaxonomy) at call time; nil means "no taxonomy module".

type TxTermBackend

type TxTermBackend interface {
	SetContentTermsTx(ctx context.Context, tx *sql.Tx, contentID string, terms map[string][]string) error
}

TxTermBackend is optionally implemented by the taxonomy service so content can write an item's terms inside its own save transaction.

type Type

type Type struct {
	Slug         string            `json:"slug"`
	Name         map[string]string `json:"name"`
	Singular     map[string]string `json:"singular"`
	Plural       map[string]string `json:"plural"`
	Icon         string            `json:"icon"`
	Public       bool              `json:"public"`
	HasArchive   bool              `json:"has_archive"`
	Hierarchical bool              `json:"hierarchical"`
	System       bool              `json:"system"`
	ArchiveSlug  string            `json:"archive_slug"`
	Supports     []string          `json:"supports"`
	Fields       []Field           `json:"fields"`
	Taxonomies   []string          `json:"taxonomies"`
	Templates    []string          `json:"templates"`
	MenuPosition int               `json:"menu_position"`
	CreatedAt    string            `json:"created_at"`
	UpdatedAt    string            `json:"updated_at"`
}

type ValidationError

type ValidationError struct {
	Fields FieldErrors `json:"fields"`
}

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error implements error. ValidationError is always returned as a pointer (*ValidationError); match it with errors.As(err, &ve) where ve is *ValidationError. Field values are i18n keys (e.g. "content.validation.required").

Jump to

Keyboard shortcuts

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