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
- Variables
- func DirectPageReadAllowed(ctx context.Context, id string) bool
- func EditToken(it *Item) string
- func EditTokenInTx(ctx context.Context, tx *sql.Tx, id string) (string, error)
- func GuardUnapprovedMutation(ctx context.Context) context.Context
- func ParseDatetime(v string) (time.Time, bool)
- func Policy() *bluemonday.Policy
- func PubliclyReadable(ctx context.Context, svc Service, id string) bool
- func Sanitize(html string) string
- func Slugify(s string, asciiOnly bool) string
- func WithDirectPageRead(ctx context.Context, id string) context.Context
- func WithPublicExclusions(ctx context.Context, ids []string) context.Context
- type AtomicUpdater
- type BatchPublisher
- type Comment
- type CommentInput
- type CommentQuery
- type CommentService
- type CreateInput
- type Field
- type FieldErrors
- type FieldType
- type Filter
- type Input
- type Item
- type Module
- type Option
- type Order
- type Page
- type PageAccess
- type PageAccessInput
- type PageAccessService
- type PatchInput
- type PublishExpectation
- type Query
- type Revision
- type SaveAuthorization
- type Service
- type Status
- type TermBackend
- type TxTermBackend
- type Type
- type ValidationError
Constants ¶
const ( CommentPending = "pending" CommentApproved = "approved" CommentSpam = "spam" CommentTrash = "trash" )
Comment statuses.
const PermModerateComments = "comments.moderate"
PermModerateComments lets a user approve, mark as spam, trash and delete comments; their own comments are approved at once.
const ServiceComments = "content.comments"
ServiceComments is the kernel service name of the CommentService.
Variables ¶
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") )
var CommentStatuses = []string{CommentPending, CommentApproved, CommentSpam, CommentTrash}
CommentStatuses are the comment statuses, in moderation order.
var ErrConflict = errors.New("content: changed since it was loaded")
var ErrNotFound = errors.New("content: not found")
var ErrPublishConflict = errors.New("content: reviewed content has changed")
var FieldTypes = []FieldType{FieldText, FieldTextarea, FieldRichtext, FieldNumber, FieldDate, FieldDatetime, FieldBoolean, FieldSelect, FieldMultiselect, FieldRepeater, FieldRelationship, FieldImage, FieldFile, FieldJSON}
FieldTypes lists every field type in builder order.
var Statuses = []Status{"draft", "pending", "scheduled", "published", "private", "trash"}
Statuses lists every item status.
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 EditToken ¶
EditToken is an opaque optimistic version of persisted content. Unlike second-resolution timestamps, it detects two edits within the same second.
func EditTokenInTx ¶
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 ¶
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 ¶
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 ¶
PubliclyReadable is separate from staff read authorization. A visitor's unlock grants the direct page, never searchable excerpts or shared caches.
func Sanitize ¶
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 ¶
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.
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 `json:"locale,omitempty" doc:"Defaults to the configured site locale."`
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 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 ¶
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).
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 ¶
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.
type PageAccess ¶
type PageAccessInput ¶
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 ¶
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 ¶
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 SaveAuthorization ¶
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 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").
Source Files
¶
- admin.go
- admin_atomic.go
- admin_comments.go
- admin_content.go
- admin_fields.go
- admin_translations.go
- admin_types.go
- api.go
- api_input.go
- atomic_update.go
- batch_publish.go
- comments.go
- contract.go
- doc.go
- errors.go
- index.go
- module.go
- mutation_guard.go
- page_access.go
- query.go
- read_permissions.go
- relations.go
- revisions.go
- sanitize.go
- save_authorization.go
- schedule.go
- search.go
- seo_drift.go
- slug.go
- status.go
- store.go
- translation_input.go
- translations.go
- translations_guarded.go
- types.go
- validate.go