Files
summercms/modules/pact/content.go
Jakub Zych 561a56c768 feat(signpost): content descriptor fields for dates, parents, authors, media and outline
- pact.ContentDescriptor gains Kind and Tools with the ContentKind and
  ContentTool constants
- pact.ContentRecord gains PublishedAt, Parents, Authors and TopHeading;
  pact.ContentLocale gains Images, Videos and Headings, with the value
  types ContentRef, ContentPerson, ContentImage, ContentVideo and
  ContentHeading; every field is optional
- TestContentDescriptorFields proves signpost hands them through and
  that a type setting none of them describes as before
- the runnable content example, the pact and signpost READMEs and the
  menu item types page document the fields
2026-11-04 08:31:49 +01:00

200 lines
7.3 KiB
Go

package pact
import (
"context"
"time"
)
// ContentDescriptor describes the records behind a menu item type, for
// consumers that work on every content type at once: sitemaps, search
// indexers, audit tools, metadata editors. A type sets it in
// MenuItemType.Content; a type without one takes no part and behaves as
// before.
//
// Controller is the ID of the admin controller whose form edits the
// records, "" when none does. Model is the records' morph type, the value
// RecordSaved and RecordDeleted carry (lagoon.MorphType), so an event maps to
// its content type with one lookup; it is required. Describe is required and
// describes one record by id. Records pages through the records and Match
// maps a public path to a record; both are optional.
//
// The functions read through cabana.TxFromContext(ctx) when ctx carries a
// write transaction, so code running inside a save (a form extension's Save,
// for example) sees the values the save has just written.
//
// Kind and Tools are optional facts about the type as a whole: what kind of
// thing a record is (one of the ContentKind constants, "" when the type does
// not say) and which MCP tools change its records, keyed by operation (the
// ContentTool constants), so a consumer can point at a fixing tool without
// importing the content plugin.
type ContentDescriptor struct {
Controller string
Model string
Records func(ctx context.Context, in ContentList) (ContentPage, error) `json:"-"`
Describe func(ctx context.Context, in ContentDescribe) (*ContentRecord, error) `json:"-"`
Match func(ctx context.Context, in ContentMatch) (*ContentMatchResult, error) `json:"-"`
// Kind is what a record of the type is, one of the ContentKind constants;
// "" when the type does not say.
Kind string
// Tools maps an operation (a ContentTool constant) to the full name of
// the MCP tool that performs it on a record, such as "acme_update_page";
// nil when the type names none.
Tools map[string]string
}
// The kinds a ContentDescriptor.Kind names.
const (
// ContentKindArticle is a dated piece with authors, such as a post.
ContentKindArticle = "article"
// ContentKindCollection is a record that lists other records, such as a
// category or a tag.
ContentKindCollection = "collection"
// ContentKindProfile is a record about a person or an organisation.
ContentKindProfile = "profile"
// ContentKindPage is a standalone page.
ContentKindPage = "page"
)
// The operations a ContentDescriptor.Tools map is keyed by.
const (
// ContentToolEdit names the tool that edits a record's fields and body.
ContentToolEdit = "edit"
// ContentToolHeadings names the tool that changes the headings of a
// record's body.
ContentToolHeadings = "headings"
// ContentToolImageAlt names the tool that sets the alternative text of
// the images in a record's body.
ContentToolImageAlt = "image_alt"
)
// ContentList is what ContentDescriptor.Records receives. Pattern is the URL
// pattern already layered by the registry (the host's override, else the
// type's default). After is the ID of the last record of the previous page,
// "" for the first page; Limit is the page size the caller wants (the type
// may cap it). UnprefixedLocale names the locale whose URLs carry no locale
// prefix; "" leaves the choice to the type.
type ContentList struct {
Pattern string
After string
Limit int
UnprefixedLocale string
}
// ContentPage is one page of records in a stable order. Next is the After of
// the following page, "" on the last page.
type ContentPage struct {
Records []ContentRecord
Next string
}
// ContentDescribe is what ContentDescriptor.Describe receives: the record's
// ID, the layered URL pattern and the unprefixed locale, as in ContentList.
type ContentDescribe struct {
ID string
Pattern string
UnprefixedLocale string
}
// ContentRecord describes one record. ID is its primary key as text,
// Published whether visitors can see it, LastMod its last change (nil when
// unknown). Locales lists the record per locale in the site's locale order.
// The remaining fields are optional; a nil slice and an empty one mean the
// same: the type has nothing to say.
type ContentRecord struct {
ID string
Published bool
LastMod *time.Time
Locales []ContentLocale
// PublishedAt is the record's first publication, nil when unknown or
// never published.
PublishedAt *time.Time
// Parents lists the records this one sits under, in order of preference:
// the first is the default primary parent.
Parents []ContentRef
// Authors lists the people who wrote the record, in byline order.
Authors []ContentPerson
// TopHeading is the highest heading level the record's body may use: 2
// when the page renders the title as its only level-1 heading, 0 when the
// type sets no rule.
TopHeading int
}
// ContentLocale is a record in one locale. URL is its public path, "" when
// the locale or the host has no public page for it. Title, Description and
// Image (a URL) are the record's own defaults for page metadata; Words is the
// body's word count, nil when the type has no body. Images, Videos and
// Headings are optional; a nil slice and an empty one mean the same.
type ContentLocale struct {
Locale string
URL string
Title string
Description string
Image string
Words *int
// Images lists the record's images in this locale (the featured image
// first, then the body's in document order).
Images []ContentImage
// Videos lists the videos the body embeds, in document order.
Videos []ContentVideo
// Headings is the body's heading outline in document order.
Headings []ContentHeading
}
// ContentRef points at another record: Type is a menu item type key and ID
// the record's ID within it.
type ContentRef struct {
Type string
ID string
}
// ContentPerson is a person named on a record. Ref points at the person's
// own record; its Type is "" when the person has none.
type ContentPerson struct {
Name string
Ref ContentRef
}
// ContentImage is an image a record shows. URL is its address and Alt its
// alternative text; Decorative marks an image that carries no content, whose
// empty Alt is intended. Width and Height are in pixels, 0 when unknown.
type ContentImage struct {
URL string
Alt string
Decorative bool
Width int
Height int
}
// ContentVideo is a video a record embeds: Provider names the host (such as
// "youtube") and ID the video there.
type ContentVideo struct {
Provider string
ID string
}
// ContentHeading is one heading of a record's body: Level 1 to 6 and its
// text.
type ContentHeading struct {
Level int
Text string
}
// ContentMatch is what ContentDescriptor.Match receives: a public path (no
// query, no host), the layered URL pattern and the unprefixed locale.
type ContentMatch struct {
Path string
Pattern string
UnprefixedLocale string
}
// ContentMatchResult names the record a path belongs to and the locale the
// path addresses. Exact is false when the path names the record but a
// segment does not match it (another category, a stale locale prefix, a
// slug of another locale): the caller may then redirect to the record's URL.
// Match answers nil when the path names no record visitors can see.
type ContentMatchResult struct {
ID string
Locale string
Exact bool
}