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
This commit is contained in:
@@ -36,6 +36,7 @@ Use cases:
|
||||
- Optional admin hooks a controller or model can implement: list and form query scoping (`pact.ListExtendQuery`, `pact.FormExtendQuery`), list row states (`pact.ListRowStates` with the fixed `pact.RowState` set `pact.RowStateDeleted`, `pact.RowStateNegative` and `pact.RowStateDisabled`), values of unstored list columns filled once per page (`pact.ListExtendRecords`), the record title of the form and preview screens (`pact.FormRecordTitle`), create, update and delete hooks (`pact.FormBeforeCreate`, `pact.FormAfterUpdate`, `pact.FormBeforeDelete` and their siblings), a hook after a reorder save (`pact.ReorderAfterSave`), form-only fields that reach those hooks without being model columns (`pact.FormVirtualFields`), validation rules per operation for admin saves (`pact.FormRules`), relation hooks (`pact.RelationExtendManageQuery`, `pact.RelationExtendOptionsQuery`, `pact.RelationBeforeLink`), relation child hooks around creating, updating and deleting a related record (`pact.RelationBeforeCreate`, `pact.RelationAfterCreate`, `pact.RelationBeforeUpdate`, `pact.RelationAfterUpdate`, `pact.RelationBeforeDelete`, `pact.RelationAfterDelete`), filter scopes (`pact.FilterScope`, `pact.FilterOptions`) and dropdown options (`pact.DropdownOptionsProvider`).
|
||||
- Dependent dropdown options: `pact.DependentOptionsProvider` answers a `pact.DependentOptionSet` (whether the sibling's value takes a choice at all, and the choices) for a dropdown with `dependsOn`, from the submitted values of its siblings and with a context, so the choices can come from the database; the admin framework calls it for the options route and again on every save. `pact.Option.Depth` indents a choice taken from a tree.
|
||||
- Menu item types: `pact.HasMenuItemTypes` returns `pact.MenuItemType` values (key, label, default URL pattern, `PatternFrom` for a list type, the `References` an item can point at as a `pact.MenuItemReference` tree, and `Resolve`), the typed successor of WinterCMS's `pages.menuitem.*` events. `Resolve` receives a `pact.MenuItemResolve` with the pattern already layered and answers a `pact.ResolvedMenuItem` with its alternates as an ordered `[]pact.MenuItemAlternate`. [signpost](../signpost/README.md) collects them at boot. A record type may set `Content` to a `pact.ContentDescriptor`, which describes its records for consumers that work on every content type at once (sitemaps, search indexers, metadata editors): the admin controller that edits them, their morph type, `Describe` (a `pact.ContentRecord` with per-locale `pact.ContentLocale` URL, title, description, image and word count), and the optional `Records` (paged through `pact.ContentList` and `pact.ContentPage`) and `Match` (a public path to a `pact.ContentMatchResult`).
|
||||
- Content facts: a descriptor may also say what kind of record the type holds (`pact.ContentDescriptor.Kind`) and which MCP tools change its records, by operation (`pact.ContentDescriptor.Tools`); `Describe` and `Records` may report a record's first publication, ordered parents, authors and top heading level, and per locale its images, videos and heading outline. All of them are optional.
|
||||
- Record events: `pact.RecordSaved` and `pact.RecordDeleted` are fired as pointers on the application's event bus after a record write commits, with the controller, the morph type, the id, the request locale and, for a save, the `pact.RecordChange` list of changed attributes with old and new values per locale. The admin framework fires them for every admin write and `cabana.EmitRecordSaved` lets plugin code that writes directly fire the same events.
|
||||
- Singleton admin controllers: `pact.AdminSingleton` (`SingletonRecord`, first or create) makes a controller edit exactly one record, opened directly instead of from a list.
|
||||
- A background job contract (`pact.Job`, `pact.JobArgs`) that does not depend on any queue library.
|
||||
@@ -213,12 +214,19 @@ func (p *Plugin) Schedule() []pact.ScheduledCommand {
|
||||
| `pact.MenuItemResolve` | What `Resolve` receives: type, reference, the layered URL pattern, whether to expand nested items and the unprefixed locale. |
|
||||
| `pact.ResolvedMenuItem` | A resolved item: title, URL (empty for a pure list type), modification time, ordered alternates and nested items. |
|
||||
| `pact.MenuItemAlternate` | The URL and title of a resolved item in one locale. |
|
||||
| `pact.ContentDescriptor` | The records behind a menu item type: `Controller` (the admin controller ID that edits them), `Model` (their morph type, required), `Describe` (required), and the optional `Records` and `Match`. The funcs read through `cabana.TxFromContext` when the context carries a write transaction. |
|
||||
| `pact.ContentDescriptor` | The records behind a menu item type: `Controller` (the admin controller ID that edits them), `Model` (their morph type, required), `Describe` (required), the optional `Records` and `Match`, `Kind` (one of the kind constants below, "" when unsaid) and `Tools` (operation to full MCP tool name). The funcs read through `cabana.TxFromContext` when the context carries a write transaction. |
|
||||
| `pact.ContentKindArticle`, `pact.ContentKindCollection`, `pact.ContentKindProfile`, `pact.ContentKindPage` | The values of `ContentDescriptor.Kind`: a dated piece with authors, a record that lists others, a person or organisation, a standalone page. |
|
||||
| `pact.ContentToolEdit`, `pact.ContentToolHeadings`, `pact.ContentToolImageAlt` | The keys of `ContentDescriptor.Tools`: edit a record, change its body's headings, set its images' alternative text. |
|
||||
| `pact.ContentList` | What `Records` receives: the layered pattern, `After` (the last ID of the previous page), `Limit` and the unprefixed locale. |
|
||||
| `pact.ContentPage` | One page of `Records`, and `Next`, the `After` of the following page ("" on the last). |
|
||||
| `pact.ContentDescribe` | What `Describe` receives: the record `ID`, the layered pattern and the unprefixed locale. |
|
||||
| `pact.ContentRecord` | A described record: `ID`, `Published`, `LastMod` and its `Locales`. |
|
||||
| `pact.ContentLocale` | A record in one locale: `URL` ("" without a public page), `Title`, `Description`, `Image` and `Words`. |
|
||||
| `pact.ContentRecord` | A described record: `ID`, `Published`, `LastMod` and its `Locales`, and optionally `PublishedAt` (first publication), `Parents` (ordered, the first is the default primary), `Authors` and `TopHeading` (the highest heading level the body may use, 0 for no rule). |
|
||||
| `pact.ContentLocale` | A record in one locale: `URL` ("" without a public page), `Title`, `Description`, `Image` and `Words`, and optionally `Images`, `Videos` and `Headings`. |
|
||||
| `pact.ContentRef` | Another record: a menu item type key (`Type`) and the record `ID`. |
|
||||
| `pact.ContentPerson` | A person named on a record: `Name` and a `Ref` to their own record (`Type` "" when they have none). |
|
||||
| `pact.ContentImage` | An image a record shows: `URL`, `Alt`, `Decorative` (no content, empty `Alt` intended), `Width` and `Height` (0 when unknown). |
|
||||
| `pact.ContentVideo` | A video a record embeds: `Provider` and the video `ID` there. |
|
||||
| `pact.ContentHeading` | One heading of a record's body: `Level` (1 to 6) and `Text`. |
|
||||
| `pact.ContentMatch` | What `Match` receives: a public `Path`, the layered pattern and the unprefixed locale. |
|
||||
| `pact.ContentMatchResult` | `Match`'s answer: the record `ID`, the `Locale` the path addresses and `Exact` (false when a segment does not match the record). |
|
||||
| `pact.RecordSaved` | Event fired as a pointer after a record create or update commits: `Controller` ("" outside the admin forms), `Model` (morph type), `ID`, `Locale`, `Created` and `Changes`. |
|
||||
|
||||
@@ -21,14 +21,52 @@ import (
|
||||
// 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,
|
||||
@@ -60,17 +98,32 @@ type ContentDescribe struct {
|
||||
// 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.
|
||||
// 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
|
||||
@@ -78,6 +131,52 @@ type ContentLocale struct {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user