Documentation
¶
Overview ¶
Package entity provides a schema-driven JSON-LD generator that reads x-schema-org-type and x-schema-org-property annotations from JSON Schema definitions to produce structured data conforming to Schema.org vocabulary.
Package entity — plugin extension point for JSON-LD generation.
Core ships this registry EMPTY: no default builders are registered here for any schema slug. Plugins that add a content type (e.g. case-study, team-member) register their own JSON-LD builder via RegisterJSONLDBuilder from their Wire().
Package entity provides entity domain models and repository interfaces for the Entity Management system.
Package entity provides entity domain models and repository interfaces.
Package entity — plugin extension point for public entity URL construction, used by JSON-LD generation (BuildEntityURLForJSONLD).
Modeled on jsonld_hooks.go's registration pattern. Core ships this registry EMPTY: no default builders are registered here for any schema slug. Plugins that add a content type register their own URL builder via RegisterEntityURLBuilder from their Wire().
Index ¶
- Constants
- Variables
- func AllowedInlineHref(raw string) bool
- func ApplyCanonicalToJSONLD(doc map[string]any, schemaSlug, canonical string)
- func BuildEntityURLForJSONLD(ent *Entity, baseURL string) string
- func CheckSanitisedLength(doc *builder.Doc) error
- func DerivedAttributes(raw json.RawMessage) json.RawMessage
- func EligibleForPageDoc(sc *EntitySchema) bool
- func EnrichJSONLDWithEditorial(jsonLD map[string]any, editorial json.RawMessage)
- func EnrichJSONLDWithEvidence(jsonLD map[string]any, links []EvidenceLinkForJSONLD)
- func EntityRole(ctx context.Context, st BuilderStore, e *Entity) string
- func FlattenAttributes(raw json.RawMessage) map[string]string
- func GenerateAICrawlerComment(src AICommentSource, tenantName, baseURL string) string
- func GenerateJSONLD(entity *Entity, baseURL string) (map[string]any, error)
- func GenerateJSONLDWithFAQ(ent *Entity, baseURL string, faqItems []FAQItem) (map[string]any, error)
- func GenerateSchemaDrivernJSONLD(ent *Entity, baseURL string) map[string]any
- func IframeSrcAllowed(src string) bool
- func IsAllowedSchemaOrgType(t string) bool
- func IsBlockSchema(sc *EntitySchema) bool
- func IsPartSchema(sc *EntitySchema) bool
- func IsPostEditorSchema(sc *EntitySchema) bool
- func IsReuseSchema(sc *EntitySchema) bool
- func IsReuseSlug(slug string) bool
- func LegacyPageDoc(attrs json.RawMessage) (*builder.Doc, bool)
- func LegacyPageDocErr(attrs json.RawMessage) (*builder.Doc, error)
- func LoadDoc(ctx context.Context, st BuilderStore, id, slug string) (*builder.Doc, bool)
- func NormalizeAttributes(stored, in json.RawMessage) (json.RawMessage, error)
- func NormalizeLocale(tag string) (string, error)
- func PersonDescription(shortBio, bio string) string
- func PlainText(markup string, n int) string
- func RegisterEntityURLBuilder(schemaSlug string, fn EntityURLBuilder)
- func RegisterJSONLDBuilder(schemaSlug string, fn JSONLDBuilder)
- func ReuseRole(sc *EntitySchema) string
- func SanitizeAttributesAgainst(raw, stored json.RawMessage) (json.RawMessage, error)
- func SanitizeDocComponents(doc *builder.Doc, olds ...*builder.Doc) bool
- func SanitizeHTMLSectionBody(s string) string
- func SanitizeHTMLSections(raw json.RawMessage) (json.RawMessage, int, error)
- func SanitizeInlineHTML(s string) string
- func SanitizeRichText(s string) string
- func SanitizeSectionContent(sectionType string, content map[string]any) map[string]any
- func SanitizeStoredAttributes(raw json.RawMessage) (json.RawMessage, int, error)
- func SaveBuilderDoc(ctx context.Context, st BuilderStore, actor *uuid.UUID, e *Entity, ...) (*SavedDoc, *DocProblem, error)
- func SchemaOrgTypeForEntity(ent *Entity) string
- func SectionsFromAttributes(raw json.RawMessage) ([]builder.Section, error)
- func SetFieldKindResolver(r FieldKindResolver)
- func SetJSONLDEnricher(f JSONLDEnricher)
- func SetURLRegistry(r *URLRegistry)
- func ValidAttrKey(k string) bool
- func ValidStatus(s EntityStatus) bool
- func ValidateEditorSetting(slug string, schema, settings json.RawMessage) error
- func ValidateSchemaSettings(raw json.RawMessage) error
- func ValidateSchemaSettingsForWrite(raw json.RawMessage) error
- func ValidateSchemaShape(raw json.RawMessage) error
- type AICommentSource
- type AttrValueCount
- type BeforeSaveFunc
- type BeforeSaveInput
- type BreadcrumbPathItem
- type BuilderStore
- type Byline
- type BylineResolver
- type CreateEntityInput
- type CreateSchemaInput
- type Descendant
- type DocProblem
- type DocState
- type EditorSetting
- type EditorialProvenance
- type Entity
- type EntityLinkSync
- type EntityLocation
- type EntityRepository
- type EntitySchema
- type EntityStatus
- type EntityURLBuilder
- type EntityVersion
- type Event
- type EventKind
- type EventSink
- type EvidenceLinkForJSONLD
- type FAQItem
- type FieldKindResolver
- type HierarchyStore
- type ImportBatch
- type ImportItem
- type ImportItemResult
- type ImportRecord
- type ImportResult
- type ImportStore
- type ImportSummary
- type JSONLDBuilder
- type JSONLDEnricher
- type KeysetPage
- type ListEntitiesFilter
- type ListFilter
- type LiveFunc
- type OffsetPage
- type OffsetPagedResult
- type PagedResult
- type ParentCount
- type PathChangeFunc
- type PostgresEntityRepository
- func (r *PostgresEntityRepository) Ancestors(ctx context.Context, id uuid.UUID) ([]BreadcrumbPathItem, error)
- func (r *PostgresEntityRepository) Child(ctx context.Context, parent *uuid.UUID, slug, schemaSlug string) (*Entity, error)
- func (r *PostgresEntityRepository) CountByParent(ctx context.Context, limit int) ([]ParentCount, error)
- func (r *PostgresEntityRepository) CountBySchemaStatus(ctx context.Context) ([]SchemaStatusCount, error)
- func (r *PostgresEntityRepository) CountChildren(ctx context.Context, parentIDs []uuid.UUID) (map[uuid.UUID]int, error)
- func (r *PostgresEntityRepository) Create(ctx context.Context, userID *uuid.UUID, input *CreateEntityInput) (*Entity, error)
- func (r *PostgresEntityRepository) CreateVersion(ctx context.Context, entityID uuid.UUID, version int, ...) (*EntityVersion, error)
- func (r *PostgresEntityRepository) Delete(ctx context.Context, id uuid.UUID) error
- func (r *PostgresEntityRepository) Descendants(ctx context.Context, id uuid.UUID) ([]*Descendant, error)
- func (r *PostgresEntityRepository) GetByID(ctx context.Context, id uuid.UUID) (*Entity, error)
- func (r *PostgresEntityRepository) GetByImportKey(ctx context.Context, key string) (*Entity, error)
- func (r *PostgresEntityRepository) GetBySlug(ctx context.Context, slug string) (*Entity, error)
- func (r *PostgresEntityRepository) GetVersion(ctx context.Context, entityID uuid.UUID, version int) (*EntityVersion, error)
- func (r *PostgresEntityRepository) HasChildren(ctx context.Context, id uuid.UUID) (bool, error)
- func (r *PostgresEntityRepository) ImportUpsert(ctx context.Context, rec *ImportRecord) (*Entity, bool, error)
- func (r *PostgresEntityRepository) List(ctx context.Context, filter *ListEntitiesFilter, page *KeysetPage) (*PagedResult[*Entity], error)
- func (r *PostgresEntityRepository) ListVersions(ctx context.Context, entityID uuid.UUID, limit int) ([]*EntityVersion, error)
- func (r *PostgresEntityRepository) ListWithOffset(ctx context.Context, filter *ListEntitiesFilter, page *OffsetPage) (*OffsetPagedResult[*Entity], error)
- func (r *PostgresEntityRepository) PruneVersions(ctx context.Context, keep int) (int64, error)
- func (r *PostgresEntityRepository) ScanMentioning(ctx context.Context, id uuid.UUID, fn func(*Entity) bool) error
- func (r *PostgresEntityRepository) Schedule(ctx context.Context, id uuid.UUID, userID *uuid.UUID, at time.Time) (*Entity, error)
- func (r *PostgresEntityRepository) SubtreeHeight(ctx context.Context, id uuid.UUID) (int, error)
- func (r *PostgresEntityRepository) Update(ctx context.Context, id uuid.UUID, userID *uuid.UUID, input *UpdateEntityInput) (*Entity, error)
- func (r *PostgresEntityRepository) UpdateStatus(ctx context.Context, id uuid.UUID, userID *uuid.UUID, status EntityStatus) (*Entity, error)
- type PostgresPublicRepository
- func (r *PostgresPublicRepository) GetAncestorSlugs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID][]string, error)
- func (r *PostgresPublicRepository) GetEntityPath(ctx context.Context, entityID uuid.UUID) ([]BreadcrumbPathItem, error)
- func (r *PostgresPublicRepository) GetHomepage(ctx context.Context) (*Entity, error)
- func (r *PostgresPublicRepository) GetPublishedByID(ctx context.Context, entityID uuid.UUID) (*Entity, error)
- func (r *PostgresPublicRepository) GetPublishedByIDs(ctx context.Context, ids []uuid.UUID) ([]*Entity, error)
- func (r *PostgresPublicRepository) GetPublishedByPath(ctx context.Context, segments []string, schemaSlugs []string) (*Entity, error)
- func (r *PostgresPublicRepository) GetPublishedBySlug(ctx context.Context, slug string) (*Entity, error)
- func (r *PostgresPublicRepository) GetPublishedBySlugAndSchema(ctx context.Context, slug string, schemaSlug string) (*Entity, error)
- func (r *PostgresPublicRepository) GetPublishedBySlugAndSchemas(ctx context.Context, slug string, schemaSlugs []string) (*Entity, error)
- func (r *PostgresPublicRepository) ListAllPublished(ctx context.Context) ([]*SitemapEntry, error)
- func (r *PostgresPublicRepository) ListAllPublishedWithAttrs(ctx context.Context, attrKeys []string) ([]*SitemapEntry, error)
- func (r *PostgresPublicRepository) ListAttrValues(ctx context.Context, schemaSlugs []string, attrKey string, limit int) ([]AttrValueCount, error)
- func (r *PostgresPublicRepository) ListPublished(ctx context.Context, schemaSlug string, page, perPage int) (*PublicPagedResult, error)
- func (r *PostgresPublicRepository) ListPublishedByAttribute(ctx context.Context, schemaSlugs []string, attrKey string, attrValue string, ...) ([]*Entity, int, error)
- func (r *PostgresPublicRepository) ListPublishedByAuthor(ctx context.Context, schemaSlugs []string, memberSlug, memberName string, ...) (*PublicPagedResult, error)
- func (r *PostgresPublicRepository) ListPublishedBySchemas(ctx context.Context, schemaSlugs []string, page, perPage int) (*PublicPagedResult, error)
- func (r *PostgresPublicRepository) ListPublishedBySchemasNoCount(ctx context.Context, schemaSlugs []string, limit int) ([]*Entity, error)
- func (r *PostgresPublicRepository) ListPublishedFiltered(ctx context.Context, f ListFilter, page, perPage int) ([]*Entity, int, error)
- func (r *PostgresPublicRepository) ListSeriesRows(ctx context.Context, schemaSlugs []string, seriesSlug string) ([]SeriesRow, error)
- type PostgresSchemaRepository
- func (r *PostgresSchemaRepository) CountEntities(ctx context.Context, schemaID uuid.UUID) (int, error)
- func (r *PostgresSchemaRepository) Create(ctx context.Context, input *CreateSchemaInput) (*EntitySchema, error)
- func (r *PostgresSchemaRepository) Delete(ctx context.Context, id uuid.UUID) error
- func (r *PostgresSchemaRepository) GetByID(ctx context.Context, id uuid.UUID) (*EntitySchema, error)
- func (r *PostgresSchemaRepository) GetBySlug(ctx context.Context, slug string) (*EntitySchema, error)
- func (r *PostgresSchemaRepository) List(ctx context.Context) ([]*EntitySchema, error)
- func (r *PostgresSchemaRepository) Update(ctx context.Context, id uuid.UUID, input *UpdateSchemaInput) (*EntitySchema, error)
- type PublicPagedResult
- type PublicRepository
- type RefUser
- type ReuseStatus
- type SQLDB
- type SaveOpts
- type SavedDoc
- type SchemaDefinition
- type SchemaLister
- type SchemaPatternFunc
- type SchemaProperty
- type SchemaRepository
- type SchemaSitemapSettings
- type SchemaStatusCount
- type SchemaURLConfig
- type SchemaURLOverride
- type SchemaURLSettings
- type SeriesRow
- type Service
- func (s *Service) ComputeDiff(ctx context.Context, entityID uuid.UUID, fromVersion, toVersion int) ([]VersionDiff, error)
- func (s *Service) CountEntitiesByParent(ctx context.Context, limit int) ([]ParentCount, error)
- func (s *Service) CountEntitiesBySchemaStatus(ctx context.Context) ([]SchemaStatusCount, error)
- func (s *Service) CountEntityChildren(ctx context.Context, parentIDs []uuid.UUID) (map[uuid.UUID]int, error)
- func (s *Service) CreateEntity(ctx context.Context, userID *uuid.UUID, input *CreateEntityInput) (*Entity, error)
- func (s *Service) CreateSchema(ctx context.Context, input *CreateSchemaInput) (*EntitySchema, error)
- func (s *Service) DeleteEntity(ctx context.Context, id uuid.UUID) error
- func (s *Service) DeleteSchema(ctx context.Context, id uuid.UUID) error
- func (s *Service) GetEntity(ctx context.Context, id uuid.UUID) (*Entity, error)
- func (s *Service) GetEntityByParentSlug(ctx context.Context, parent *uuid.UUID, slug, schemaSlug string) (*Entity, error)
- func (s *Service) GetEntityBySlug(ctx context.Context, slug string) (*Entity, error)
- func (s *Service) GetSchema(ctx context.Context, id uuid.UUID) (*EntitySchema, error)
- func (s *Service) GetSchemaBySlug(ctx context.Context, slug string) (*EntitySchema, error)
- func (s *Service) GetVersion(ctx context.Context, entityID uuid.UUID, version int) (*EntityVersion, error)
- func (s *Service) Import(ctx context.Context, actor *uuid.UUID, batch ImportBatch) (*ImportResult, error)
- func (s *Service) ListEntities(ctx context.Context, filter *ListEntitiesFilter, page *KeysetPage) (*PagedResult[*Entity], error)
- func (s *Service) ListEntitiesWithOffset(ctx context.Context, filter *ListEntitiesFilter, page *OffsetPage) (*OffsetPagedResult[*Entity], error)
- func (s *Service) ListSchemas(ctx context.Context) ([]*EntitySchema, error)
- func (s *Service) ListVersions(ctx context.Context, entityID uuid.UUID, limit int) ([]*EntityVersion, error)
- func (s *Service) RefUsers(ctx context.Context, blockID uuid.UUID, limit int) (users []RefUser, capped bool, err error)
- func (s *Service) RestoreVersion(ctx context.Context, entityID uuid.UUID, userID *uuid.UUID, version int) (*Entity, error)
- func (s *Service) ReuseStatus() ReuseStatus
- func (s *Service) ScheduleEntity(ctx context.Context, id uuid.UUID, userID *uuid.UUID, at time.Time) (*Entity, error)
- func (s *Service) SetBeforeSave(f BeforeSaveFunc)
- func (s *Service) SetBylines(b BylineResolver)
- func (s *Service) SetEvents(e EventSink)
- func (s *Service) SetLinkSync(sync EntityLinkSync)
- func (s *Service) SetLiveObserver(f LiveFunc)
- func (s *Service) SetOnSchemaChange(fn func())
- func (s *Service) SetPathObserver(f PathChangeFunc)
- func (s *Service) SetReuseStatus(st ReuseStatus)
- func (s *Service) SetSchemaPatternObserver(f SchemaPatternFunc)
- func (s *Service) URLRegistry() *URLRegistry
- func (s *Service) UpdateEntity(ctx context.Context, id uuid.UUID, userID *uuid.UUID, input *UpdateEntityInput) (*Entity, error)
- func (s *Service) UpdateEntityStatus(ctx context.Context, id uuid.UUID, userID *uuid.UUID, status EntityStatus) (*Entity, error)
- func (s *Service) UpdateSchema(ctx context.Context, id uuid.UUID, input *UpdateSchemaInput) (*EntitySchema, error)
- func (s *Service) ValidateEntityAttributes(ctx context.Context, schemaID uuid.UUID, attributes json.RawMessage) *ValidationResult
- type SettingsError
- type SitemapEntry
- type URLRegistry
- type URLSnapshot
- func (s *URLSnapshot) BuildURL(schemaSlug string, attrs map[string]string, v urlpattern.Vars) (string, bool, error)
- func (s *URLSnapshot) Config(schemaSlug string) (*SchemaURLConfig, bool)
- func (s *URLSnapshot) Configs() []*SchemaURLConfig
- func (s *URLSnapshot) EntityPath(ent *Entity, ancestors []string) string
- func (s *URLSnapshot) IsSectionPage(schemaSlug, slug string) bool
- func (s *URLSnapshot) Locate(schemaSlug string, attrs map[string]string, v urlpattern.Vars) (loc EntityLocation, found bool, err error)
- func (s *URLSnapshot) Location() *time.Location
- func (s *URLSnapshot) NeedsAncestors(schemaSlug string, attrs map[string]string) bool
- func (s *URLSnapshot) NeedsEntityVars(schemaSlug string) bool
- func (s *URLSnapshot) OverrideAttributes() []string
- func (s *URLSnapshot) PostArchivePath() string
- func (s *URLSnapshot) PostPath(slug string) string
- func (s *URLSnapshot) Router() *urlpattern.Router
- func (s *URLSnapshot) SectionPageSchemas() []string
- func (s *URLSnapshot) SectionPrefixes() map[string]bool
- func (s *URLSnapshot) VarsFor(ent *Entity, ancestors []string) urlpattern.Vars
- func (s *URLSnapshot) VarsForSitemap(e *SitemapEntry) urlpattern.Vars
- type UpdateEntityInput
- type UpdateSchemaInput
- type UpdateStatusInput
- type ValidationError
- type ValidationResult
- type Validator
- type VersionConflictError
- type VersionDiff
Constants ¶
const ( RolePage = "page" RoleBlock = "block" RolePart = "part" )
Editor roles: what the builder is editing.
const ( BlockSchemaSlug = "block" PartSchemaSlug = "theme_part" )
The two built-in types of the builder's reuse feature. Both hold a page_doc and are never public: a block is embedded by reference (a ref node), a theme part fills a layout slot of an opted-in theme.
const ( MaxArchiveTitleLen = 200 MaxArchiveDescriptionLen = 500 )
Length limits (in characters, after trimming) for the archive settings.
const AutosaveSummary = "Autosaved in the blog editor"
AutosaveSummary is the change summary of a background save; versions with it are pruned before any explicit version.
const AutosaveWindow = 5 * time.Minute
AutosaveWindow is how long one person's autosaves share a version.
const EditorPost = "post"
EditorPost is the settings.editor value that opts a type into the post editor.
const EmbedHostsEnv = "HATAMOTO_EMBED_HOSTS"
EmbedHostsEnv names the environment variable that lists the hosts an iframe may embed (comma-separated host names, no scheme or port).
const HiddenPersonSQL = `` /* 196-byte string literal not displayed */
HiddenPersonSQL is false for a person entity (schema aliased s, entity e) whose attributes.public is not true, and for any entity of the built-in block or theme_part type whatever its status (they are used by reference, never served). Every public read adds it, so these are absent from pages, sitemaps, llms.txt, search, theme queries and MCP public search alike.
It also hides every entity whose locale is not the site language: the public site serves one language, so other locales are stored but not public.
const MaxImportBatch = 1000
MaxImportBatch bounds the items of one Import call; bigger sources are sent in several batches (each emits its own import.completed).
const MaxListOffset = 1000
MaxListOffset caps ListFilter.Offset.
const MaxRefUsers = 200
MaxRefUsers caps the usage list of a block.
const NoIndexSQL = `` /* 178-byte string literal not displayed */
NoIndexSQL is true for an entity row aliased e that must stay out of sitemaps and search: metadata.no_index, attributes.no_index or an attributes.seo.robots value containing "noindex".
const PersonSchemaSlug = "person"
PersonSchemaSlug is the slug of the built-in person type, the one built-in type a site may extend (add fields, change URL and archive settings).
Variables ¶
var ( ErrImportUnsupported = errors.New("the entity repository does not support import") ErrImportEmpty = errors.New("import batch is empty") ErrImportTooLarge = fmt.Errorf("import batch has more than %d items", MaxImportBatch) )
Import errors.
var ( ErrSchemaNotFound = errors.New("entity schema not found") ErrEntityNotFound = errors.New("entity not found") ErrVersionConflict = errors.New("entity was saved by someone else") ErrSlugAlreadyExists = errors.New("slug already exists") ErrInvalidStatus = errors.New("invalid entity status") ErrInvalidSchema = errors.New("invalid JSON schema") ErrValidationFailed = errors.New("attribute validation failed") ErrVersionNotFound = errors.New("entity version not found") // ErrRestoreInvalid marks a version whose attributes no longer fit the // entity type's current schema. ErrRestoreInvalid = errors.New("version does not fit the current schema") ErrCannotDeleteBuiltin = errors.New("cannot delete built-in schema") ErrCannotModifyBuiltin = errors.New("cannot modify built-in schema") ErrSchemaInUse = errors.New("schema is in use by entities") ErrParentNotFound = errors.New("parent entity not found") ErrParentCycle = errors.New("parent would make the entity its own ancestor") ErrParentTooDeep = errors.New("entity nesting is too deep") ErrAmbiguousSlug = errors.New("several entities of different schemas share this slug: say which schema") ErrHasChildren = errors.New("entity has child entities: move or delete them first") ErrAuthorNotFound = errors.New("author not found") // ErrInvalidDoc marks an attributes.page_doc that fails validation. It does // not wrap ErrValidationFailed: the API maps it to 422, not 400. ErrInvalidDoc = errors.New("invalid page_doc") // ErrPageSectionsRemoved rejects a write that changes the layout through // page_sections on a page that has a page_doc (maps to 422). ErrPageSectionsRemoved = errors.New("page_sections can no longer change a page that has a page_doc; send attributes.page_doc (docs/BUILDER.md)") )
Common errors.
var AllowedSchemaOrgTypes = []string{
"Article", "BlogPosting", "NewsArticle", "DefinedTerm", "Person", "Service",
"Course", "HowTo", "WebPage", "CreativeWork", "Organization", "LocalBusiness",
}
AllowedSchemaOrgTypes is the allowlist for settings.schema_org_type.
var ErrRejectedByPlugin = errors.New("save rejected by plugin")
ErrRejectedByPlugin is returned (wrapped) when a before_save hook rejects a save.
var ErrScheduleInPast = errors.New("scheduled time must be in the future")
ErrScheduleInPast is returned when a scheduled time is not in the future.
var PersonCoreFields = []string{
"job_title", "short_bio", "bio", "photo", "public", "public_email",
"website", "linkedin", "x", "github", "expertise", "on_team",
}
PersonCoreFields are the properties the person type ships with. Code reads them (profiles, bylines, JSON-LD, the profile form), so a site can neither remove nor retype them. Kept equal to the seeded schema by a test.
Functions ¶
func AllowedInlineHref ¶
AllowedInlineHref reports whether raw is a link target the inline profile keeps: http, https, mailto, tel, sms or a relative reference (path, ?query, #fragment). It is the render safeURL rule (render cannot be imported here); internal/entity/testdata/inline_href_cases.json keeps the two in step.
func ApplyCanonicalToJSONLD ¶
ApplyCanonicalToJSONLD makes the entity's own node agree with the page's canonical URL: url becomes canonical, "@id" keeps its fragment (or defaults to the lower-cased @type) on top of canonical, and nested references to the previously emitted url are rewritten. Organization/company schemas keep their own url (the "website" attribute), and @graph documents are skipped.
func BuildEntityURLForJSONLD ¶
BuildEntityURLForJSONLD constructs the canonical URL for an entity. Exported so callers in other packages can resolve internal-source citation URLs when building JSON-LD payloads.
func CheckSanitisedLength ¶
CheckSanitisedLength refuses a doc whose html prop grew past the schema's maxLength while it was sanitised (the doc was validated before). Storing it would leave a doc that no longer parses.
func DerivedAttributes ¶
func DerivedAttributes(raw json.RawMessage) json.RawMessage
DerivedAttributes is the read-side view of a page's attributes for consumers that want section-shaped data (public JSON, plugins, hooks): page_sections is the sections of page_doc as of now, never the frozen stored mirror. raw is returned untouched when it has no page_doc; an invalid one drops page_sections (fail closed, as the renderer does); a builder-only doc (v1 cannot express it) has no page_sections, so the key is removed. Pure.
func EligibleForPageDoc ¶
func EligibleForPageDoc(sc *EntitySchema) bool
EligibleForPageDoc reports whether entities of sc take part in page_sections to page_doc conversion: everything except post-editor types and persons, whose page_sections (if any) is ordinary data.
func EnrichJSONLDWithEditorial ¶
func EnrichJSONLDWithEditorial(jsonLD map[string]any, editorial json.RawMessage)
EnrichJSONLDWithEditorial adds editorial provenance data to JSON-LD output. It reads the entity's Editorial field and adds editor and citation properties.
func EnrichJSONLDWithEvidence ¶
func EnrichJSONLDWithEvidence(jsonLD map[string]any, links []EvidenceLinkForJSONLD)
EnrichJSONLDWithEvidence adds claim/evidence annotations to JSON-LD output. It adds:
- "citation" entries (Schema.org CreativeWork) for every external evidence URL
- "mentions" entries (Schema.org Claim) for every field with evidence, each carrying its own per-field "citation" subset
- "evidenceCount" and "evidenceLevel" aggregate signals (non-standard but readable by LLM crawlers)
- "lastReviewed" timestamp from the most recent verification
Note: this does not emit ClaimReview rich-result markup — that requires the page to be a dedicated fact-check page. Plain Claim under mentions is the maximally compatible structure that survives audit by quality raters and gets picked up by AI Overview citation pipelines.
func EntityRole ¶
func EntityRole(ctx context.Context, st BuilderStore, e *Entity) string
EntityRole is the editor role of e, from its schema (loaded when e has none).
func FlattenAttributes ¶
func FlattenAttributes(raw json.RawMessage) map[string]string
FlattenAttributes returns the top-level scalar attributes as strings, the form pattern overrides compare against.
func GenerateAICrawlerComment ¶
func GenerateAICrawlerComment(src AICommentSource, tenantName, baseURL string) string
GenerateAICrawlerComment builds the HTML comment block that we embed in every public entity page for AI/LLM crawlers. The block is a structured, human-readable summary that doesn't depend on Schema.org parsing.
Returns an empty string when the editor has opted the entity out via the SEO tab's "Include in /llms.txt" toggle — same flag that gates the /llms.txt and /api/v1/public/knowledge-graph feeds, so the surface is consistent for crawlers.
tenantName is the human-readable site/agency name (e.g. "Acme"). baseURL is the canonical site origin (e.g. "https://example.com"), used to build the absolute llms.txt URL in the footer of the comment.
func GenerateJSONLD ¶
GenerateJSONLD generates JSON-LD structured data for an entity. It first attempts the schema-driven generator (using x-schema-org-* annotations). If the schema lacks annotations, it falls back to the legacy switch-based generator. Supports: WebPage, Article, LocalBusiness, FAQPage, Service, Person, Organization, Product.
func GenerateJSONLDWithFAQ ¶
GenerateJSONLDWithFAQ produces JSON-LD, and if FAQ items are provided, wraps the result in a @graph array that includes both the entity's own JSON-LD and a FAQPage object.
func GenerateSchemaDrivernJSONLD ¶
GenerateSchemaDrivernJSONLD produces JSON-LD by walking the schema annotations. It reads x-schema-org-type from the root to determine @type, then recursively maps attribute values using x-schema-org-property annotations on each property.
Returns nil if the schema has no x-schema-org-type annotation (caller should fall back to the legacy switch-based generator).
func IframeSrcAllowed ¶
IframeSrcAllowed reports whether src is an iframe source the sanitiser policies keep.
func IsAllowedSchemaOrgType ¶
IsAllowedSchemaOrgType reports whether t is in AllowedSchemaOrgTypes.
func IsBlockSchema ¶
func IsBlockSchema(sc *EntitySchema) bool
IsBlockSchema reports whether sc is the built-in block type.
func IsPartSchema ¶
func IsPartSchema(sc *EntitySchema) bool
IsPartSchema reports whether sc is the built-in theme part type.
func IsPostEditorSchema ¶
func IsPostEditorSchema(sc *EntitySchema) bool
IsPostEditorSchema reports whether sc is edited in the post editor: a built-in post slug, or a schema whose settings.editor is "post".
func IsReuseSchema ¶
func IsReuseSchema(sc *EntitySchema) bool
IsReuseSchema reports whether sc is the built-in block or theme part type. A site type that took the slug is not one: it keeps its normal behaviour.
func IsReuseSlug ¶
IsReuseSlug reports whether slug is the slug of a block or a theme part.
func LegacyPageDoc ¶
func LegacyPageDoc(attrs json.RawMessage) (*builder.Doc, bool)
LegacyPageDoc converts the page_sections stored in attrs into a doc, the same deterministic conversion a write applies, without touching anything. ok is false when attrs has no page_sections array or it does not convert. The caller decides whether a page_doc also exists.
func LegacyPageDocErr ¶
func LegacyPageDocErr(attrs json.RawMessage) (*builder.Doc, error)
LegacyPageDocErr is LegacyPageDoc with the reason: (nil, nil) when attrs has no page_sections array, the conversion error when it does not convert.
func NormalizeAttributes ¶
func NormalizeAttributes(stored, in json.RawMessage) (json.RawMessage, error)
NormalizeAttributes is the page_doc write seam as a pure function: the attributes UpdateEntity would store for in against the stored ones. `builder convert` verifies the render of exactly this result and writes it verbatim.
func NormalizeLocale ¶
NormalizeLocale returns the canonical BCP 47 form of tag, exactly as the site language setting canonicalizes it (language.Parse then String): "_" becomes "-", case is fixed and deprecated codes are replaced ("iw" is "he", "tl" is "fil"). The public filter compares entity locale to the site language as strings, so the two must never disagree. An invalid or over-long tag wraps ErrValidationFailed.
func PersonDescription ¶
PersonDescription is the plain-text description of a person: the short bio, else the bio with its markup removed, cut to 300 runes.
func PlainText ¶
PlainText turns stored HTML into plain text for a card excerpt: block boundaries become spaces, script and style content is dropped, entities are decoded and whitespace is collapsed. It keeps at most n runes. The result is only ever rendered escaped.
func RegisterEntityURLBuilder ¶
func RegisterEntityURLBuilder(schemaSlug string, fn EntityURLBuilder)
RegisterEntityURLBuilder installs a URL builder for the given schema slug. Intended to be called from a plugin's Wire().
func RegisterJSONLDBuilder ¶
func RegisterJSONLDBuilder(schemaSlug string, fn JSONLDBuilder)
RegisterJSONLDBuilder installs a JSON-LD builder for the given schema slug. Intended to be called from a plugin's Wire().
func ReuseRole ¶
func ReuseRole(sc *EntitySchema) string
ReuseRole is what the builder edits for a schema of the entity.
func SanitizeAttributesAgainst ¶
func SanitizeAttributesAgainst(raw, stored json.RawMessage) (json.RawMessage, error)
SanitizeAttributesAgainst is the exported form of sanitizeAttributesAgainst for write paths outside this package (the render tests use it).
func SanitizeDocComponents ¶
SanitizeDocComponents applies the write-boundary sanitiser to the content of every component node of doc (rich-text `content` strings and html-section bodies) and to the `html` prop of every richtext, html, heading and text node. Values equal to the node with the same id in one of olds (the first match wins; nil entries are ignored) are left alone, as for page_sections. It reports whether anything changed.
func SanitizeHTMLSectionBody ¶
SanitizeHTMLSectionBody sanitizes the body of an html section, or any HTML fragment a theme builds, with the html-section policy and then strips every data-hm-* attribute. The policy keeps class, id, role, style (checked), aria-* and data-* attributes; it never keeps script, event handlers, forms or an iframe off the embed allowlist (see EmbedHostsEnv).
func SanitizeHTMLSections ¶
func SanitizeHTMLSections(raw json.RawMessage) (json.RawMessage, int, error)
SanitizeHTMLSections rewrites attributes.page_sections[type=html or an HTML_SECTION_TYPE_ALIASES type].content.body to its sanitised (canonical) form. It returns the new attributes and the number of sections whose body changed. When nothing changed the input is returned verbatim. Only changed sections (and the containers on the path to them) are re-marshalled, with key order preserved and every other value byte-identical. (Postgres jsonb re-orders keys on storage regardless.) Malformed or unexpected shapes are left alone.
func SanitizeInlineHTML ¶
SanitizeInlineHTML sanitizes the `html` prop of a heading or text node: strong em b i u s code br and links, nothing else. Links keep href (only an AllowedInlineHref one; otherwise the link is unwrapped and its text kept) and target=_blank, which always carries rel="noopener noreferrer". The result is re-parsed in a <div> and rendered again, so tags are balanced and sanitising twice changes nothing.
func SanitizeRichText ¶
SanitizeRichText sanitizes rich text with the rich-text policy and then strips every data-hm-* attribute.
func SanitizeSectionContent ¶
SanitizeSectionContent returns a sanitized deep copy of a page section's content for render-time use (e.g. draft preview of unsaved attributes). For html-type sections content.body is treated as html; any string stored under a key named "content" is treated as rich text, matching the write path.
func SanitizeStoredAttributes ¶
func SanitizeStoredAttributes(raw json.RawMessage) (json.RawMessage, int, error)
SanitizeStoredAttributes is the raw-SQL maintenance form of the write-boundary sanitiser (sanitize-html-sections): html section bodies in page_sections plus the component content of page_doc. A changed doc counts as one in the returned total. The frozen page_sections mirror is only cleaned in place (never created, never re-derived from the doc). Nothing changed returns the input verbatim. Malformed shapes are left alone.
func SaveBuilderDoc ¶
func SaveBuilderDoc(ctx context.Context, st BuilderStore, actor *uuid.UUID, e *Entity, doc json.RawMessage, opts SaveOpts) (*SavedDoc, *DocProblem, error)
SaveBuilderDoc stores doc as e's page_doc: the shared checks, a merge into the stored attributes and UpdateEntity (which sanitises and normalises), then optionally the publish status change. The version is checked by the store when opts.ExpectedVersion is set. A refusal of the document is a *DocProblem; other errors come from the store.
func SchemaOrgTypeForEntity ¶
SchemaOrgTypeForEntity returns the Schema.org @type that best matches an entity's schema slug, used when emitting internal-source citation blocks. Falls back to "CreativeWork" for unknown slugs.
func SectionsFromAttributes ¶
func SectionsFromAttributes(raw json.RawMessage) ([]builder.Section, error)
SectionsFromAttributes decodes attributes.page_sections with the shared reader, numbers kept exact (used by `builder convert`).
func SetFieldKindResolver ¶
func SetFieldKindResolver(r FieldKindResolver)
SetFieldKindResolver registers the active theme's field-kind lookup. The render engine depends on this package, so the server wires it at boot rather than entity importing render. nil clears it.
Save-time schema sanitising exists only while a resolver is registered, and the server registers one only when Go theme rendering is on (a theme loaded). With rendering off there is no schema to consult, so only the schema-free passes run on save; schema-marked fields are then not sanitised until a theme is enabled and the content is saved again (render-time sanitising still protects the served HTML).
func SetJSONLDEnricher ¶
func SetJSONLDEnricher(f JSONLDEnricher)
SetJSONLDEnricher installs (or, with nil, clears) the enricher. Package level because GenerateJSONLD is a plain function with four call sites.
func SetURLRegistry ¶
func SetURLRegistry(r *URLRegistry)
SetURLRegistry installs the registry used by BuildEntityURLForJSONLD. The server calls it once at startup; without it the defaults apply.
func ValidAttrKey ¶
ValidAttrKey reports whether k can be used as a filter or order attribute.
func ValidateEditorSetting ¶
func ValidateEditorSetting(slug string, schema, settings json.RawMessage) error
ValidateEditorSetting is the write-time check of settings.editor against the schema it applies to: the post editor writes attributes.content as a string, so the schema must declare one, and the person schema has its own form.
func ValidateSchemaSettings ¶
func ValidateSchemaSettings(raw json.RawMessage) error
ValidateSchemaSettings checks the structural rules for schema settings: the value must be a JSON object, patterns must start with "/" and contain {slug} (see urlpattern.Parse), and archive_path must start with "/".
func ValidateSchemaSettingsForWrite ¶
func ValidateSchemaSettingsForWrite(raw json.RawMessage) error
ValidateSchemaSettingsForWrite is ValidateSchemaSettings plus the checks that apply only when settings are saved. schema_org_type is validated here and not on load: a stale or hand-edited value must only disable JSON-LD typing (the generator re-checks the allowlist), never reset the schema's URL routing. The same holds for editor, which is also checked only here.
func ValidateSchemaShape ¶
func ValidateSchemaShape(raw json.RawMessage) error
ValidateSchemaShape enforces editor-renderability invariants on a user-authored JSON Schema. It is intentionally narrower than the JSON Schema spec — it codifies what the visual schema builder and the field-renderer can round-trip without surprise.
Rules enforced:
- Payload is at most maxSchemaBytes (64 KiB) — anti-DoS guard.
- Root parses as a JSON object.
- Root has a `type` field and the value is one of supportedSchemaTypes.
- If root type is `object`, `properties` must be present and an object.
- Every property has a `type` of a supported kind.
- Nested `object` properties recurse with the same rules.
- `array` properties must declare `items` that is itself a valid property shape, AND items.type must NOT be `array` (repeaters are single-level only by design — the UI cannot render nested repeaters, and the raw-JSON escape hatch must agree).
- `additionalProperties` and `patternProperties` are rejected outright — both bypass the closed-shape contract that the field renderer and downstream consumers rely on. Admins should add fields via `properties`, not escape hatches.
- Property names match `^[A-Za-z_][A-Za-z0-9_]*$` AND are not on the prototype-pollution denylist (`__proto__`, `constructor`, etc.).
Errors are wrapped with ErrInvalidSchema and include a dotted path (e.g. `properties.faq.items.type missing`) so the admin builder can highlight the offending field.
Types ¶
type AICommentSource ¶
type AICommentSource struct {
Name string
Slug string
SchemaSlug string
Attributes json.RawMessage
UpdatedAt time.Time
}
AICommentSource is the minimal entity-shaped data the comment generator needs, adapted from an *Entity by AICommentSourceFromEntity.
func AICommentSourceFromEntity ¶
func AICommentSourceFromEntity(ent *Entity) AICommentSource
FromEntity adapts a published *Entity into an AICommentSource.
type AttrValueCount ¶
AttrValueCount is one distinct attribute value and how many published entities carry it.
type BeforeSaveFunc ¶
type BeforeSaveFunc func(ctx context.Context, in *BeforeSaveInput) error
BeforeSaveFunc runs before an entity is persisted. A returned error rejects the save (wrapped with ErrRejectedByPlugin by the service).
It runs outside the repository transaction: the transaction lives inside the repository's Create/Update calls.
type BeforeSaveInput ¶
type BeforeSaveInput struct {
Operation string
ID *uuid.UUID
SchemaSlug string
Name string
Slug string
// Locale is the entity's BCP 47 tag, informational only (never read back).
// Empty on create or import when the caller did not set one: the entity then
// takes the site language.
Locale string
Attributes json.RawMessage
}
BeforeSaveInput is the draft handed to the before_save hook. Operation is "create", "update" or "restore"; ID is nil for create. The hook may mutate Name, Slug and Attributes in place, or return an error to reject the save.
type BreadcrumbPathItem ¶
type BreadcrumbPathItem struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
}
BreadcrumbPathItem represents a single item in an entity's ancestor chain.
type BuilderStore ¶
type BuilderStore interface {
GetEntity(ctx context.Context, id uuid.UUID) (*Entity, error)
GetSchema(ctx context.Context, id uuid.UUID) (*EntitySchema, error)
UpdateEntity(ctx context.Context, id uuid.UUID, userID *uuid.UUID, in *UpdateEntityInput) (*Entity, error)
UpdateEntityStatus(ctx context.Context, id uuid.UUID, userID *uuid.UUID, status EntityStatus) (*Entity, error)
}
BuilderStore is what the shared save needs of the entity service.
type Byline ¶
Byline is who a post is credited to: a person entity, or the site organisation. Name is the display name (the company name for the organisation).
type BylineResolver ¶
type BylineResolver interface {
// Resolve maps byline text to a credit: the site organisation when it is
// the company name, else the person with that slug or name, creating a
// non-public person without a login when there is none.
Resolve(ctx context.Context, name string) (Byline, error)
// Name is the display name of the credit: the person's name, or the
// organisation's when personID is nil.
Name(ctx context.Context, personID *uuid.UUID) (string, error)
// Check returns ErrAuthorNotFound when id is not a person.
Check(ctx context.Context, id uuid.UUID) error
// Mirror returns the hatamoto_authors row that mirrors the person (made on
// first use). author_id is dual-written with it so a rollback still shows
// bylines.
Mirror(ctx context.Context, personID uuid.UUID) (*uuid.UUID, error)
// PersonChanged is called after a person entity was created or saved: it
// refreshes the mirror row and the plain-text byline on their posts.
PersonChanged(ctx context.Context, personID uuid.UUID) error
// PersonDeleting is called before a person entity is deleted: their posts
// lose the reference and keep the plain-text byline.
PersonDeleting(ctx context.Context, personID uuid.UUID) error
}
BylineResolver is the slice of the people store the entity service needs. People are entities of the `person` type, but this package never imports internal/person.
type CreateEntityInput ¶
type CreateEntityInput struct {
SchemaID uuid.UUID `json:"schema_id"`
Name string `json:"name"`
Slug string `json:"slug,omitempty"` // Auto-generated if empty
Locale string `json:"locale,omitempty"` // BCP 47; empty stores the site language
Attributes json.RawMessage `json:"attributes"`
Metadata json.RawMessage `json:"metadata,omitempty"`
ParentID *uuid.UUID `json:"parent_id,omitempty"`
AuthorID *uuid.UUID `json:"author_id,omitempty"`
// BylinePersonID and BylineOrg set the byline (see Entity).
BylinePersonID *uuid.UUID `json:"byline_person_id,omitempty"`
BylineOrg bool `json:"byline_org,omitempty"`
// Status is set by the service for a person or a block or part (see lockedStatus); a caller
// cannot choose it. Unset stores a draft.
Status *EntityStatus `json:"-"`
}
CreateEntityInput represents input for creating a new entity.
type CreateSchemaInput ¶
type CreateSchemaInput struct {
Name string `json:"name"`
Slug string `json:"slug,omitempty"` // Auto-generated if empty
Description *string `json:"description,omitempty"`
Schema json.RawMessage `json:"schema"`
Settings json.RawMessage `json:"settings,omitempty"`
}
CreateSchemaInput represents input for creating a new entity schema.
type Descendant ¶
type Descendant struct {
Entity *Entity
// Between holds the slugs of the rows between the starting entity and
// this one, root first (empty for a direct child).
Between []string
}
Descendant is a published row below another entity.
type DocProblem ¶
DocProblem is a refusal of a document: an HTTP-style status, a stable code and the message. Msg is not yet phrased as a sentence for people.
func CheckDocFor ¶
func CheckDocFor(ctx context.Context, st BuilderStore, e *Entity, doc json.RawMessage) (*builder.Doc, string, *DocProblem)
CheckDocFor parses doc and runs every rule a stored page_doc of e must meet before the entity service sees it: structure, settings, slot and reference rules. It returns the parsed doc.
func CheckReuseDoc ¶
func CheckReuseDoc(role string, e *Entity, d *builder.Doc, lookup func(id string) (*builder.Doc, bool)) *DocProblem
CheckReuseDoc applies the save rules of the reuse types to a doc about to be stored on e: a slot belongs to layout parts only (and a single or archive part has exactly one), and references must not loop. lookup reads a block's doc.
func ParseDoc ¶
func ParseDoc(doc json.RawMessage) (*builder.Doc, *DocProblem)
ParseDoc parses doc and checks its settings: the first rules every stored page_doc meets.
func (*DocProblem) Error ¶
func (p *DocProblem) Error() string
type DocState ¶
type DocState int
DocState is what an entity's stored page_doc is to the editor choice.
type EditorSetting ¶
type EditorSetting string
EditorSetting is settings.editor as stored. It decodes leniently: a value that is not a JSON string reads as "", so a stale or hand-edited setting can never make URLSettings fail and reset the schema's URL routing.
func (*EditorSetting) UnmarshalJSON ¶
func (e *EditorSetting) UnmarshalJSON(b []byte) error
UnmarshalJSON implements json.Unmarshaler.
type EditorialProvenance ¶
type EditorialProvenance struct {
ReviewedBy string `json:"reviewed_by,omitempty"`
ReviewedAt string `json:"reviewed_at,omitempty"`
FactChecked bool `json:"fact_checked,omitempty"`
Sources []string `json:"sources,omitempty"`
LastVerifiedAt string `json:"last_verified_at,omitempty"`
}
EditorialProvenance represents the editorial metadata stored on an entity.
type Entity ¶
type Entity struct {
ID uuid.UUID `json:"id"`
SchemaID uuid.UUID `json:"schema_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Locale string `json:"locale"`
Status EntityStatus `json:"status"`
Attributes json.RawMessage `json:"attributes"`
Metadata json.RawMessage `json:"metadata,omitempty"`
Editorial json.RawMessage `json:"editorial,omitempty"`
Version int `json:"version"`
CreatedBy *uuid.UUID `json:"created_by,omitempty"`
UpdatedBy *uuid.UUID `json:"updated_by,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PublishedAt *time.Time `json:"published_at,omitempty"`
// Hierarchy and content classification fields
ParentID *uuid.UUID `json:"parent_id,omitempty"`
// AuthorID references hatamoto_authors (a byline, not a user account).
AuthorID *uuid.UUID `json:"author_id,omitempty"`
// BylinePersonID is the person entity credited as the author; BylineOrg
// credits the site organisation instead. They are exclusive. This is the
// source of bylines; AuthorID is only dual-written so a rollback still
// renders them. BylinePersonID has no foreign key: integrity is kept in code.
BylinePersonID *uuid.UUID `json:"byline_person_id,omitempty"`
BylineOrg bool `json:"byline_org,omitempty"`
// Joined fields (not stored in DB)
Schema *EntitySchema `json:"schema,omitempty"`
}
Entity represents a data entity following a schema.
type EntityLinkSync ¶
type EntityLinkSync interface {
// Reindex refreshes hatamoto_entity_link_index for sourceEntityID based on
// whatever <a data-entity-id> tags currently live in its stored content.
// See internal/linking/v2/indexer.go.
Reindex(ctx context.Context, sourceEntityID uuid.UUID) error
// RewriteForEntity rewrites the href of every <a data-entity-id="X">
// across the codebase to newCanonicalURL when X's own slug changes. See internal/linking/v2/rewriter.go.
RewriteForEntity(ctx context.Context, targetEntityID uuid.UUID, newCanonicalURL, reason string, actor *uuid.UUID) error
// ResolveCanonical returns the post-update canonical URL for the entity
// whose slug changed, so the rewriter has a destination href.
ResolveCanonical(ent *Entity) (string, error)
}
EntityLinkSync is the v2 linking surface the entity service calls after a successful update. Kept as an interface so:
- entity does not need to import internal/linking/v2 (one-way dep)
- tests can pass a noop and assert without DB
Both methods must accept a fresh context.Background() — the request context the HTTP handler holds is cancelled when the response writes, and the v2 sync runs in a goroutine that outlives the request.
type EntityLocation ¶
type EntityLocation struct {
// Path is the canonical URL path ("/blog/x", "/about").
Path string
// Section is true for a section landing page served at /{slug}.
Section bool
// ArchivePath and ArchiveName describe the section index crumb that
// precedes the entity; empty when the entity has none (no archive_path,
// a section page, or a per-entity override).
ArchivePath string
ArchiveName string
}
EntityLocation is where one entity lives on the public site.
type EntityRepository ¶
type EntityRepository interface {
Create(ctx context.Context, userID *uuid.UUID, input *CreateEntityInput) (*Entity, error)
GetByID(ctx context.Context, id uuid.UUID) (*Entity, error)
GetBySlug(ctx context.Context, slug string) (*Entity, error)
Update(ctx context.Context, id uuid.UUID, userID *uuid.UUID, input *UpdateEntityInput) (*Entity, error)
UpdateStatus(ctx context.Context, id uuid.UUID, userID *uuid.UUID, status EntityStatus) (*Entity, error)
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, filter *ListEntitiesFilter, page *KeysetPage) (*PagedResult[*Entity], error)
ListWithOffset(ctx context.Context, filter *ListEntitiesFilter, page *OffsetPage) (*OffsetPagedResult[*Entity], error)
CountBySchemaStatus(ctx context.Context) ([]SchemaStatusCount, error)
CountByParent(ctx context.Context, limit int) ([]ParentCount, error)
CountChildren(ctx context.Context, parentIDs []uuid.UUID) (map[uuid.UUID]int, error)
// Version operations
CreateVersion(ctx context.Context, entityID uuid.UUID, version int, attributes json.RawMessage, changedBy *uuid.UUID, changeSummary *string) (*EntityVersion, error)
PruneVersions(ctx context.Context, keep int) (int64, error)
GetVersion(ctx context.Context, entityID uuid.UUID, version int) (*EntityVersion, error)
ListVersions(ctx context.Context, entityID uuid.UUID, limit int) ([]*EntityVersion, error)
}
EntityRepository defines the interface for entity data operations.
type EntitySchema ¶
type EntitySchema struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Description *string `json:"description,omitempty"`
Schema json.RawMessage `json:"schema"`
IsBuiltin bool `json:"is_builtin"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Settings holds optional per-schema routing configuration (see
// SchemaURLSettings). An empty object means "use the built-in defaults".
Settings json.RawMessage `json:"settings"`
}
EntitySchema represents a JSON Schema definition for entities.
func (*EntitySchema) URLSettings ¶
func (s *EntitySchema) URLSettings() (SchemaURLSettings, error)
URLSettings decodes Settings. An empty or null value yields zero settings.
type EntityStatus ¶
type EntityStatus string
EntityStatus represents the status of an entity.
const ( StatusDraft EntityStatus = "draft" // StatusScheduled holds an entity that is finished but not yet due. // The public repository filters on status = 'published', so a scheduled // row is invisible to readers for free — no date guard is needed on the // eight public query sites. worker.PublishWorker flips it to published // once published_at falls due, preserving published_at rather than // stamping now() the way UpdateStatus does. StatusScheduled EntityStatus = "scheduled" StatusPublished EntityStatus = "published" StatusArchived EntityStatus = "archived" )
Available entity statuses.
func ParseStatus ¶
func ParseStatus(s string) (EntityStatus, error)
ParseStatus parses a string into an EntityStatus.
type EntityURLBuilder ¶
EntityURLBuilder returns the canonical public URL for an entity given the request's baseURL and the entity's slug. baseURL is per-request (Server.publicBaseURL(r): PUBLIC_BASE_URL, else the request origin) and per-tenant, so it must be supplied to the builder rather than assumed — every other branch of BuildEntityURLForJSONLD returns an absolute baseURL+path URL, and a relative path here would produce an invalid relative @id/url in JSON-LD.
func LookupEntityURLBuilder ¶
func LookupEntityURLBuilder(schemaSlug string) (EntityURLBuilder, bool)
LookupEntityURLBuilder returns the registered URL builder for the given schema slug, or ok=false when none is registered.
type EntityVersion ¶
type EntityVersion struct {
ID uuid.UUID `json:"id"`
EntityID uuid.UUID `json:"entity_id"`
Version int `json:"version"`
Attributes json.RawMessage `json:"attributes"`
ChangedBy *uuid.UUID `json:"changed_by,omitempty"`
ChangeSummary *string `json:"change_summary,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
EntityVersion represents a historical version of an entity's attributes.
type Event ¶
type Event struct {
Kind EventKind
ID uuid.UUID
SchemaSlug string
Slug string
Locale string
Name string
Status string
Version int
// Import is set for EventImportCompleted only; the entity fields are zero.
Import *ImportSummary
}
Event describes a completed entity mutation.
type EventKind ¶
type EventKind string
EventKind names an entity lifecycle event.
const ( EventCreated EventKind = "entity.created" EventUpdated EventKind = "entity.updated" EventPublished EventKind = "entity.published" EventDeleted EventKind = "entity.deleted" // EventImportCompleted is emitted once per Import batch, in place of the // per-entity created/published events. EventImportCompleted EventKind = "import.completed" )
type EventSink ¶
EventSink receives entity events after a mutation has been persisted. Emit is called inline from the service, so implementations must return quickly and hand real work to a goroutine; the request context may be cancelled as soon as the response is written.
type EvidenceLinkForJSONLD ¶
type EvidenceLinkForJSONLD struct {
FieldPath string
LinkType string
ConfidenceScore int
VerifiedAt *string // ISO 8601 timestamp, nil if not verified
SourceTitle string
SourceType string
// SourceURL is the URL used in citation blocks. For external_url sources
// this is the user-supplied URL; for internal entity-backed sources the
// server layer resolves the entity's canonical URL before passing in.
SourceURL string
// SourceURLType is the Schema.org @type of the citation block. Defaults
// to "CreativeWork" when empty. Server layer sets this to "Article" /
// "Person" / etc. for internal entity sources so LLMs and crawlers
// see a specific type instead of the generic fallback.
SourceURLType string
}
EvidenceLinkForJSONLD is a lightweight struct for passing evidence data into the JSON-LD enrichment function, avoiding a circular import of the evidence package.
type FAQItem ¶
FAQItem represents a question-answer pair for FAQ JSON-LD generation.
func ExtractFAQItems ¶
ExtractFAQItems reads the question and answer pairs of the FAQ blocks the blog editor writes into rich text:
<div class="faq"><div class="faq-item"><h3 class="faq-q">Why?</h3><div class="faq-a"><p>Because.</p></div></div></div>
Items without both a question and an answer are skipped. The public page and the editor's SEO panel both use it, so what the panel promises (FAQPage) is what the page emits.
type FieldKindResolver ¶
FieldKindResolver reports the x-hm-field kind ("richtext", "html", ...) of the section-content field at path for a section type, or "" when the type or field is unknown. path holds object keys only; array indices are skipped.
type HierarchyStore ¶
type HierarchyStore interface {
// Ancestors returns the parent chain of id, root first, without id itself.
// A chain deeper than urlpattern.MaxPathDepth is cut at that depth, which
// is also what ends a cycle.
Ancestors(ctx context.Context, id uuid.UUID) ([]BreadcrumbPathItem, error)
// Descendants returns every published descendant of id (children,
// grandchildren, ...), parents before children.
Descendants(ctx context.Context, id uuid.UUID) ([]*Descendant, error)
// SubtreeHeight is how many levels of rows (any status) lie below id: 0
// for a leaf, 1 with children only. Counting stops at twice
// urlpattern.MaxPathDepth, which is also what ends a cycle.
SubtreeHeight(ctx context.Context, id uuid.UUID) (int, error)
// HasChildren reports whether any row has id as its parent.
HasChildren(ctx context.Context, id uuid.UUID) (bool, error)
// Child returns the base row (no variant) with this slug directly under
// parent, or a root row when parent is nil; ErrEntityNotFound when none.
// Slugs are unique per schema, not among all siblings: schemaSlug narrows
// the lookup to one schema, and without it ErrAmbiguousSlug is returned
// when rows of several schemas match.
Child(ctx context.Context, parent *uuid.UUID, slug, schemaSlug string) (*Entity, error)
}
HierarchyStore reads the parent tree. PostgresEntityRepository implements it; the service uses it when its repository does.
type ImportBatch ¶
type ImportBatch struct {
// Source labels the origin ("wordpress", "archive"); it is stored in the
// entity metadata and sent in import.completed.
Source string
Items []ImportItem
}
ImportBatch is what one Import call stores.
type ImportItem ¶
type ImportItem struct {
// Key is the idempotency key, unique per source entity (for example
// "wp:post:123"). An item whose key was imported before updates that
// entity in place instead of creating another.
Key string
SchemaSlug string
Name string
Slug string // generated from Name when empty
Locale string // BCP 47 tag; empty is the site language on insert and unchanged on update
Status EntityStatus
Attributes json.RawMessage
Metadata json.RawMessage
// ParentKey names the parent by import key (a row of this batch or of an
// earlier one); ParentID by id. At most one is set.
ParentKey string
ParentID *uuid.UUID
// AuthorID is a legacy hatamoto_authors id; AuthorName resolves a person by name.
// Without either, a plain-text attributes.author is resolved like a save.
AuthorID *uuid.UUID
AuthorName string
// BylinePersonID credits a person entity; BylineOrg the site organisation.
BylinePersonID *uuid.UUID
BylineOrg bool
// Dates; zero values default to now (created/updated) and, for a
// published item, to its creation date (published).
PublishedAt, CreatedAt, UpdatedAt time.Time
}
ImportItem is one entity to import.
type ImportItemResult ¶
ImportItemResult is the outcome of one item.
type ImportRecord ¶
type ImportRecord struct {
ID *uuid.UUID // existing entity to update; nil inserts
SchemaID uuid.UUID
Name, Slug string
Status EntityStatus
Attributes json.RawMessage
Metadata json.RawMessage
ParentID *uuid.UUID
AuthorID *uuid.UUID
ActorID *uuid.UUID
BylinePersonID *uuid.UUID
BylineOrg bool
// Locale is a BCP 47 tag; empty inserts the site language and keeps the
// stored value on update.
Locale string
CreatedAt, UpdatedAt, PublishedAt time.Time
}
ImportRecord is one validated, sanitised row ready to store.
type ImportResult ¶
type ImportResult struct {
Created, Updated, Failed int
Items []ImportItemResult
}
ImportResult summarises a batch. Items are in the order of the input.
type ImportStore ¶
type ImportStore interface {
// GetByImportKey returns the entity imported under key, ErrEntityNotFound
// when there is none.
GetByImportKey(ctx context.Context, key string) (*Entity, error)
// ImportUpsert inserts rec, or updates the entity rec.ID names, keeping
// the dates rec carries (the trigger-free import path). created is true
// for an insert.
ImportUpsert(ctx context.Context, rec *ImportRecord) (e *Entity, created bool, err error)
}
ImportStore is the persistence the importer needs. PostgresEntityRepository implements it.
type ImportSummary ¶
ImportSummary is the payload of the import.completed event.
type JSONLDBuilder ¶
type JSONLDBuilder func(jsonLD map[string]any, entity *Entity, attrs map[string]any, baseURL string)
JSONLDBuilder renders JSON-LD fields for an entity of a given schema slug directly into jsonLD, mirroring the shape of the in-package buildXJSONLD helpers (e.g. buildArticleJSONLD).
func LookupJSONLDBuilder ¶
func LookupJSONLDBuilder(schemaSlug string) (JSONLDBuilder, bool)
LookupJSONLDBuilder returns the registered JSON-LD builder for the given schema slug, or ok=false when none is registered.
type JSONLDEnricher ¶
JSONLDEnricher post-processes a generated JSON-LD document in place. It is the host's single seam for the jsonld.enrich filter; GenerateJSONLD has no context, so the host binding supplies its own (see pluginhost.BindHostHooks).
type KeysetPage ¶
type KeysetPage struct {
Cursor *string `json:"cursor,omitempty"` // Base64-encoded cursor
Limit int `json:"limit"`
Direction string `json:"direction"` // "next" or "prev"
}
KeysetPage represents keyset pagination parameters.
type ListEntitiesFilter ¶
type ListEntitiesFilter struct {
SchemaID *uuid.UUID `json:"schema_id,omitempty"`
// ExcludeSchemaIDs drops the entities of these types.
ExcludeSchemaIDs []uuid.UUID `json:"exclude_schema_ids,omitempty"`
Status *EntityStatus `json:"status,omitempty"`
Search string `json:"search,omitempty"`
// AuthorID keeps entities with this byline.
AuthorID *uuid.UUID `json:"author_id,omitempty"`
// PersonID keeps entities credited to this person.
PersonID *uuid.UUID `json:"person_id,omitempty"`
// TermID keeps entities tagged with this term or one of its descendants.
TermID *uuid.UUID `json:"term_id,omitempty"`
// Under keeps the descendants (children, grandchildren, ...) of this entity.
Under *uuid.UUID `json:"under,omitempty"`
// UpdatedFrom (inclusive) and UpdatedBefore (exclusive) bound updated_at.
UpdatedFrom *time.Time `json:"updated_from,omitempty"`
UpdatedBefore *time.Time `json:"updated_before,omitempty"`
// Sort is name | status | updated | created (default updated, newest
// first); Asc flips the direction. Only the offset listing honours it.
Sort string `json:"sort,omitempty"`
Asc bool `json:"asc,omitempty"`
}
ListEntitiesFilter represents filter options for listing entities.
type ListFilter ¶
type ListFilter struct {
// SchemaSlugs restricts to these schemas (empty: any schema).
SchemaSlugs []string
// TermID restricts to entities attached to the term.
TermID uuid.UUID
// AuthorID restricts to entities written by this author.
AuthorID uuid.UUID
// PersonID restricts to entities credited to this person.
PersonID uuid.UUID
// Search is a websearch-syntax query matched against the entity's
// full-text vector (name, excerpt, content), best match first. Entities
// marked no_index never appear in search results.
Search string
// Attr restricts to entities whose attribute Attr equals AttrEq (a string,
// or the number/boolean AttrEq spells), or that have the attribute at all
// when AttrEq is empty and AttrEqSet is false. With AttrEqSet an empty
// AttrEq matches the empty string only. The match is a JSONB containment test (@>), served
// by the GIN index on attributes. An invalid key matches nothing.
Attr, AttrEq string
AttrEqSet bool
// IncludeScheduled adds scheduled entities (published_at is their date) to
// the published ones. Drafts and archived entities never appear.
IncludeScheduled bool
// Order is "" (newest first), "-published_at", "published_at", "name",
// "-name", "updated_at", "-updated_at", "attr:<key>" or "-attr:<key>" (numeric values sort as numbers,
// before the rest; entities lacking the attribute last). Anything else
// gives the default order.
Order string
// ExcludeID leaves this entity out (e.id <> ExcludeID) when set.
ExcludeID uuid.UUID
// Offset skips this many more rows on top of (page-1)*perPage; clamped to
// 0..MaxListOffset. The returned total stays the raw match count.
Offset int
}
ListFilter narrows a public listing. Only published base rows (never variants) are listed; every set field must match.
type LiveFunc ¶
LiveFunc is told about entities that may just have started to answer at their URL: created, published, or imported. The server wires it to retire the automatic redirects of their path.
type OffsetPage ¶
OffsetPage represents offset pagination parameters.
type OffsetPagedResult ¶
type OffsetPagedResult[T any] struct { Items []T `json:"items"` Total int `json:"total"` Page int `json:"page"` Limit int `json:"limit"` TotalPages int `json:"total_pages"` }
OffsetPagedResult represents a paginated result set using offset pagination.
type PagedResult ¶
type PagedResult[T any] struct { Items []T `json:"items"` NextCursor *string `json:"next_cursor,omitempty"` PrevCursor *string `json:"prev_cursor,omitempty"` HasMore bool `json:"has_more"` Total *int `json:"total,omitempty"` // Only populated when requested }
PagedResult represents a paginated result set.
type ParentCount ¶
ParentCount is one parent entity and how many entities sit directly under it.
type PathChangeFunc ¶
PathChangeFunc is told about an update that may have moved an entity (and, with it, its descendants) to another URL: a new slug or a new parent. The server wires it to create redirects and refresh internal links.
type PostgresEntityRepository ¶
type PostgresEntityRepository struct {
// contains filtered or unexported fields
}
PostgresEntityRepository implements EntityRepository using PostgreSQL.
func NewPostgresEntityRepository ¶
func NewPostgresEntityRepository(db *database.DB) *PostgresEntityRepository
NewPostgresEntityRepository creates a new PostgreSQL entity repository.
func (*PostgresEntityRepository) Ancestors ¶
func (r *PostgresEntityRepository) Ancestors(ctx context.Context, id uuid.UUID) ([]BreadcrumbPathItem, error)
Ancestors implements HierarchyStore.
func (*PostgresEntityRepository) Child ¶
func (r *PostgresEntityRepository) Child(ctx context.Context, parent *uuid.UUID, slug, schemaSlug string) (*Entity, error)
Child implements HierarchyStore.
func (*PostgresEntityRepository) CountByParent ¶
func (r *PostgresEntityRepository) CountByParent(ctx context.Context, limit int) ([]ParentCount, error)
CountByParent returns the limit parents with the most children (most first), each with its child count, in one query (the admin list's Parent filter).
func (*PostgresEntityRepository) CountBySchemaStatus ¶
func (r *PostgresEntityRepository) CountBySchemaStatus(ctx context.Context) ([]SchemaStatusCount, error)
CountBySchemaStatus counts the catalog entities per schema and status in one grouped query (the admin list's type tabs and status select).
func (*PostgresEntityRepository) CountChildren ¶
func (r *PostgresEntityRepository) CountChildren(ctx context.Context, parentIDs []uuid.UUID) (map[uuid.UUID]int, error)
CountChildren returns how many entities sit directly under each of parentIDs (redirected ones excluded, as in CountByParent); parents with none are absent.
func (*PostgresEntityRepository) Create ¶
func (r *PostgresEntityRepository) Create(ctx context.Context, userID *uuid.UUID, input *CreateEntityInput) (*Entity, error)
Create inserts a new entity.
func (*PostgresEntityRepository) CreateVersion ¶
func (r *PostgresEntityRepository) CreateVersion(ctx context.Context, entityID uuid.UUID, version int, attributes json.RawMessage, changedBy *uuid.UUID, changeSummary *string) (*EntityVersion, error)
CreateVersion creates a new version record for an entity.
The tenantID parameter is retained for API compatibility but no longer participates in the SQL (Path B Migration 079 dropped the column).
func (*PostgresEntityRepository) Descendants ¶
func (r *PostgresEntityRepository) Descendants(ctx context.Context, id uuid.UUID) ([]*Descendant, error)
Descendants implements HierarchyStore.
func (*PostgresEntityRepository) GetByImportKey ¶
GetByImportKey implements ImportStore.
func (*PostgresEntityRepository) GetVersion ¶
func (r *PostgresEntityRepository) GetVersion(ctx context.Context, entityID uuid.UUID, version int) (*EntityVersion, error)
GetVersion retrieves a specific version of an entity.
func (*PostgresEntityRepository) HasChildren ¶
HasChildren implements HierarchyStore.
func (*PostgresEntityRepository) ImportUpsert ¶
func (r *PostgresEntityRepository) ImportUpsert(ctx context.Context, rec *ImportRecord) (*Entity, bool, error)
ImportUpsert implements ImportStore.
func (*PostgresEntityRepository) List ¶
func (r *PostgresEntityRepository) List(ctx context.Context, filter *ListEntitiesFilter, page *KeysetPage) (*PagedResult[*Entity], error)
List retrieves entities with filtering and keyset pagination.
func (*PostgresEntityRepository) ListVersions ¶
func (r *PostgresEntityRepository) ListVersions(ctx context.Context, entityID uuid.UUID, limit int) ([]*EntityVersion, error)
ListVersions retrieves version history for an entity.
func (*PostgresEntityRepository) ListWithOffset ¶
func (r *PostgresEntityRepository) ListWithOffset(ctx context.Context, filter *ListEntitiesFilter, page *OffsetPage) (*OffsetPagedResult[*Entity], error)
ListWithOffset retrieves entities with filtering and offset pagination.
func (*PostgresEntityRepository) PruneVersions ¶
PruneVersions coalesces identical-consecutive versions and then keeps only the newest `keep` versions per entity. Returns the number of rows deleted.
func (*PostgresEntityRepository) ScanMentioning ¶
func (r *PostgresEntityRepository) ScanMentioning(ctx context.Context, id uuid.UUID, fn func(*Entity) bool) error
ScanMentioning calls fn, newest first, for each entity other than id whose attributes contain the id as text, until fn returns false. It is a sequence scan: used only by the block usage list, never on the render path.
func (*PostgresEntityRepository) Schedule ¶
func (r *PostgresEntityRepository) Schedule(ctx context.Context, id uuid.UUID, userID *uuid.UUID, at time.Time) (*Entity, error)
Schedule marks an entity scheduled for at: status scheduled and published_at at, which worker.PublishWorker promotes once it falls due.
func (*PostgresEntityRepository) SubtreeHeight ¶
SubtreeHeight implements HierarchyStore.
func (*PostgresEntityRepository) Update ¶
func (r *PostgresEntityRepository) Update(ctx context.Context, id uuid.UUID, userID *uuid.UUID, input *UpdateEntityInput) (*Entity, error)
Update modifies an entity (creates a version snapshot first).
func (*PostgresEntityRepository) UpdateStatus ¶
func (r *PostgresEntityRepository) UpdateStatus(ctx context.Context, id uuid.UUID, userID *uuid.UUID, status EntityStatus) (*Entity, error)
UpdateStatus changes an entity's status.
type PostgresPublicRepository ¶
type PostgresPublicRepository struct {
// contains filtered or unexported fields
}
PostgresPublicRepository implements PublicRepository using PostgreSQL.
func NewPostgresPublicRepository ¶
func NewPostgresPublicRepository(db *database.DB) *PostgresPublicRepository
NewPostgresPublicRepository creates a new public repository.
func (*PostgresPublicRepository) GetAncestorSlugs ¶
func (r *PostgresPublicRepository) GetAncestorSlugs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID][]string, error)
GetAncestorSlugs returns, for each of ids that has a parent, the slugs of its ancestors, root first (self excluded), in one recursive query. Ids without a parent are absent from the map. The walk is bounded like GetEntityPath.
func (*PostgresPublicRepository) GetEntityPath ¶
func (r *PostgresPublicRepository) GetEntityPath(ctx context.Context, entityID uuid.UUID) ([]BreadcrumbPathItem, error)
GetEntityPath returns the ancestor chain for an entity using a recursive CTE. The result is ordered from root to the entity itself (e.g., [Home, Services, SEO, Enterprise SEO]).
func (*PostgresPublicRepository) GetHomepage ¶
func (r *PostgresPublicRepository) GetHomepage(ctx context.Context) (*Entity, error)
GetHomepage retrieves the homepage entity. Resolution order:
- Explicit homepage_entity_id from tenant settings (if set and entity exists/published)
- Slug convention fallback: "home" → "homepage" → first published page
func (*PostgresPublicRepository) GetPublishedByID ¶
func (r *PostgresPublicRepository) GetPublishedByID(ctx context.Context, entityID uuid.UUID) (*Entity, error)
GetPublishedByID retrieves a published entity by ID.
func (*PostgresPublicRepository) GetPublishedByIDs ¶
func (r *PostgresPublicRepository) GetPublishedByIDs(ctx context.Context, ids []uuid.UUID) ([]*Entity, error)
GetPublishedByIDs retrieves multiple published entities by their IDs.
func (*PostgresPublicRepository) GetPublishedByPath ¶
func (r *PostgresPublicRepository) GetPublishedByPath(ctx context.Context, segments []string, schemaSlugs []string) (*Entity, error)
GetPublishedByPath retrieves the published base entity of a schema in schemaSlugs that sits at the end of a parent chain: segments are slugs, root first, the last being the entity's own. Ancestors may be of any schema and status (a draft region page still gives its children their URL). Slugs are only unique among siblings, so the chain is walked from the root.
func (*PostgresPublicRepository) GetPublishedBySlug ¶
func (r *PostgresPublicRepository) GetPublishedBySlug(ctx context.Context, slug string) (*Entity, error)
GetPublishedBySlug retrieves a published entity by slug.
func (*PostgresPublicRepository) GetPublishedBySlugAndSchema ¶
func (r *PostgresPublicRepository) GetPublishedBySlugAndSchema(ctx context.Context, slug string, schemaSlug string) (*Entity, error)
GetPublishedBySlugAndSchema retrieves a published base entity by slug, restricted to a specific schema slug.
func (*PostgresPublicRepository) GetPublishedBySlugAndSchemas ¶
func (r *PostgresPublicRepository) GetPublishedBySlugAndSchemas(ctx context.Context, slug string, schemaSlugs []string) (*Entity, error)
GetPublishedBySlugAndSchemas retrieves a published base entity by slug whose schema slug is in schemaSlugs. Deterministic when several rows share a slug.
func (*PostgresPublicRepository) ListAllPublished ¶
func (r *PostgresPublicRepository) ListAllPublished(ctx context.Context) ([]*SitemapEntry, error)
ListAllPublished retrieves all published entities for sitemap generation.
func (*PostgresPublicRepository) ListAllPublishedWithAttrs ¶
func (r *PostgresPublicRepository) ListAllPublishedWithAttrs(ctx context.Context, attrKeys []string) ([]*SitemapEntry, error)
ListAllPublishedWithAttrs implements PublicRepository.
func (*PostgresPublicRepository) ListAttrValues ¶
func (r *PostgresPublicRepository) ListAttrValues(ctx context.Context, schemaSlugs []string, attrKey string, limit int) ([]AttrValueCount, error)
ListAttrValues returns the distinct non-empty string values of the attribute attrKey across the published base entities of the given schemas, with counts, sorted by value, at most limit (1..500). Entities where the attribute is not a string are ignored. One aggregate query; the key presence test uses the GIN index on attributes.
func (*PostgresPublicRepository) ListPublished ¶
func (r *PostgresPublicRepository) ListPublished(ctx context.Context, schemaSlug string, page, perPage int) (*PublicPagedResult, error)
ListPublished retrieves paginated published base entities by schema.
func (*PostgresPublicRepository) ListPublishedByAttribute ¶
func (r *PostgresPublicRepository) ListPublishedByAttribute(ctx context.Context, schemaSlugs []string, attrKey string, attrValue string, limit int, offset int) ([]*Entity, int, error)
ListPublishedByAttribute retrieves published entities where a JSONB attribute key matches the given value. Entities are filtered to the given schema slugs, ordered by published_at DESC, and limited to `limit` results. Only attribute keys in allowedAttributeKeys are permitted for security.
func (*PostgresPublicRepository) ListPublishedByAuthor ¶
func (r *PostgresPublicRepository) ListPublishedByAuthor(ctx context.Context, schemaSlugs []string, memberSlug, memberName string, page, perPage int) (*PublicPagedResult, error)
ListPublishedByAuthor lists published base entities of schemaSlugs written by a team member, newest first, with the total. The match: author_entity_slug equals the slug; else an author object whose slug or name matches; else (no author object) an author string or author_name equal to the member's name.
func (*PostgresPublicRepository) ListPublishedBySchemas ¶
func (r *PostgresPublicRepository) ListPublishedBySchemas(ctx context.Context, schemaSlugs []string, page, perPage int) (*PublicPagedResult, error)
ListPublishedBySchemas retrieves paginated published base entities (variant rows are plugin-owned and never listed) whose schema slug is in schemaSlugs.
func (*PostgresPublicRepository) ListPublishedBySchemasNoCount ¶
func (r *PostgresPublicRepository) ListPublishedBySchemasNoCount(ctx context.Context, schemaSlugs []string, limit int) ([]*Entity, error)
ListPublishedBySchemasNoCount is the first page of ListPublishedBySchemas without the COUNT(*) query, for callers that only need the rows.
func (*PostgresPublicRepository) ListPublishedFiltered ¶
func (r *PostgresPublicRepository) ListPublishedFiltered(ctx context.Context, f ListFilter, page, perPage int) ([]*Entity, int, error)
ListPublishedFiltered returns one page of published entities matching f and the total number of matches. Without a search the order is newest first.
func (*PostgresPublicRepository) ListSeriesRows ¶
func (r *PostgresPublicRepository) ListSeriesRows(ctx context.Context, schemaSlugs []string, seriesSlug string) ([]SeriesRow, error)
ListSeriesRows returns the published and scheduled posts of schemaSlugs with attributes.series_slug = seriesSlug, oldest first. Scheduled parts are included so a series index can show what is coming; callers must not link them.
type PostgresSchemaRepository ¶
type PostgresSchemaRepository struct {
// contains filtered or unexported fields
}
PostgresSchemaRepository implements SchemaRepository using PostgreSQL.
func NewPostgresSchemaRepository ¶
func NewPostgresSchemaRepository(db *database.DB) *PostgresSchemaRepository
NewPostgresSchemaRepository creates a new PostgreSQL schema repository.
func (*PostgresSchemaRepository) CountEntities ¶
func (r *PostgresSchemaRepository) CountEntities(ctx context.Context, schemaID uuid.UUID) (int, error)
CountEntities returns the number of entities using a schema.
func (*PostgresSchemaRepository) Create ¶
func (r *PostgresSchemaRepository) Create(ctx context.Context, input *CreateSchemaInput) (*EntitySchema, error)
Create inserts a new entity schema.
func (*PostgresSchemaRepository) GetByID ¶
func (r *PostgresSchemaRepository) GetByID(ctx context.Context, id uuid.UUID) (*EntitySchema, error)
GetByID retrieves an entity schema by ID.
func (*PostgresSchemaRepository) GetBySlug ¶
func (r *PostgresSchemaRepository) GetBySlug(ctx context.Context, slug string) (*EntitySchema, error)
GetBySlug retrieves an entity schema by slug.
func (*PostgresSchemaRepository) List ¶
func (r *PostgresSchemaRepository) List(ctx context.Context) ([]*EntitySchema, error)
List retrieves all entity schemas for the tenant.
func (*PostgresSchemaRepository) Update ¶
func (r *PostgresSchemaRepository) Update(ctx context.Context, id uuid.UUID, input *UpdateSchemaInput) (*EntitySchema, error)
Update modifies an entity schema.
type PublicPagedResult ¶
type PublicPagedResult struct {
Items []*Entity `json:"items"`
Total int `json:"total"`
Page int `json:"page"`
PerPage int `json:"per_page"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrevious bool `json:"has_previous"`
}
PublicPagedResult represents a paginated result for public pages.
type PublicRepository ¶
type PublicRepository interface {
// GetPublishedBySlug retrieves a published entity by slug for a tenant.
GetPublishedBySlug(ctx context.Context, slug string) (*Entity, error)
// GetPublishedByID retrieves a published entity by ID.
GetPublishedByID(ctx context.Context, entityID uuid.UUID) (*Entity, error)
// GetPublishedByIDs retrieves multiple published entities by their IDs.
GetPublishedByIDs(ctx context.Context, ids []uuid.UUID) ([]*Entity, error)
// ListPublished retrieves paginated published base entities of one schema.
ListPublished(ctx context.Context, schemaSlug string, page, perPage int) (*PublicPagedResult, error)
// ListPublishedBySchemas is ListPublished over several schema slugs (a
// schema and its aliases).
ListPublishedBySchemas(ctx context.Context, schemaSlugs []string, page, perPage int) (*PublicPagedResult, error)
// ListAllPublished retrieves all published entities for sitemap generation.
ListAllPublished(ctx context.Context) ([]*SitemapEntry, error)
// ListAllPublishedWithAttrs is ListAllPublished that also hydrates
// SitemapEntry.Attrs with the named top-level attributes (the ones schema
// URL overrides compare against).
ListAllPublishedWithAttrs(ctx context.Context, attrKeys []string) ([]*SitemapEntry, error)
// GetHomepage retrieves the homepage entity (slug = "home" or first published page).
GetHomepage(ctx context.Context) (*Entity, error)
// GetPublishedBySlugAndSchema retrieves a published entity by slug filtered to a specific schema slug.
GetPublishedBySlugAndSchema(ctx context.Context, slug string, schemaSlug string) (*Entity, error)
// GetPublishedBySlugAndSchemas is GetPublishedBySlugAndSchema over several
// schema slugs (a schema and its aliases).
GetPublishedBySlugAndSchemas(ctx context.Context, slug string, schemaSlugs []string) (*Entity, error)
// ListPublishedByAttribute retrieves published entities where a specific JSONB attribute matches a value.
// Returns the matching entities, the total count, and any error.
ListPublishedByAttribute(ctx context.Context, schemaSlugs []string, attrKey string, attrValue string, limit int, offset int) ([]*Entity, int, error)
}
PublicRepository provides read-only access to published entities. This bypasses RLS for public-facing content.
type RefUser ¶
type RefUser struct {
ID uuid.UUID
Name, Slug string
SchemaSlug string
Status EntityStatus
UpdatedAt time.Time
}
RefUser is one entity that embeds a block: a page, another block or a part.
type ReuseStatus ¶
type ReuseStatus struct {
Block, Part bool
}
ReuseStatus says which reuse types are usable on this site. A type is unavailable when a site type of its own already owns the slug.
func EnsureReuseSchemas ¶
EnsureReuseSchemas creates the block and theme_part types as built-in rows when they are missing. It is plain data (no migration), idempotent and safe with two processes starting at once. A slug already owned by a site's own (non-built-in) type is left alone, logged, and reported unavailable.
type SQLDB ¶
type SQLDB interface {
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}
SQLDB is the slice of pgx EnsureReuseSchemas needs; *pgxpool.Pool and pgx.Tx satisfy it.
type SaveOpts ¶
type SaveOpts struct {
ChangeSummary string // "" keeps "Updated in the builder"
Autosave bool // a background save: recorded as AutosaveSummary, coalesced with the last one
Publish bool // flip the page live after the save
// ExpectedVersion makes the write atomic with the version check: the store
// compares it under its row lock and returns a *VersionConflictError.
ExpectedVersion *int
// AfterParse runs once the doc parsed and the role is known, before the
// reuse rules; BeforeWrite runs after every check, before the write. A
// non-nil result refuses the save.
AfterParse func(role string) *DocProblem
BeforeWrite func() *DocProblem
// Tune changes the update before it is written (the admin sets the title).
Tune func(in *UpdateEntityInput)
// PublishGate returns why a requested publish is skipped ("" publishes).
PublishGate func(saved *Entity) string
}
SaveOpts tunes SaveBuilderDoc. Every field is optional.
type SavedDoc ¶
type SavedDoc struct {
Entity *Entity // as the service returned it, published when it was
Parsed *builder.Doc
// TakenSlug is the address Tune asked for that was in use; the entity kept its old one.
TakenSlug string
// PublishSkipped is the PublishGate's reason; PublishErr a failed status
// change. Either way the doc is stored.
PublishSkipped string
PublishErr error
}
SavedDoc is the outcome of a stored document.
type SchemaDefinition ¶
type SchemaDefinition struct {
Type string `json:"type"`
SchemaOrgType string `json:"x-schema-org-type,omitempty"`
Properties map[string]*SchemaProperty `json:"properties,omitempty"`
Required []string `json:"required,omitempty"`
}
SchemaDefinition represents the top-level JSON Schema with semantic annotations.
type SchemaLister ¶
type SchemaLister interface {
List(ctx context.Context) ([]*EntitySchema, error)
}
SchemaLister is the slice of SchemaRepository the registry needs.
type SchemaPatternFunc ¶
type SchemaPatternFunc func(ctx context.Context, before, after *EntitySchema)
SchemaPatternFunc is told after a schema update changed where its entities live (its URL pattern or overrides). before and after are the schema as it was and as it is. The server wires it to leave a 301 at every old path.
type SchemaProperty ¶
type SchemaProperty struct {
Type string `json:"type"`
Format string `json:"format,omitempty"`
Title string `json:"title,omitempty"`
SchemaOrgType string `json:"x-schema-org-type,omitempty"`
SchemaOrgProperty string `json:"x-schema-org-property,omitempty"`
Properties map[string]*SchemaProperty `json:"properties,omitempty"`
Items *SchemaProperty `json:"items,omitempty"`
Required []string `json:"required,omitempty"`
}
SchemaProperty represents a parsed JSON Schema property with its semantic annotations.
type SchemaRepository ¶
type SchemaRepository interface {
Create(ctx context.Context, input *CreateSchemaInput) (*EntitySchema, error)
GetByID(ctx context.Context, id uuid.UUID) (*EntitySchema, error)
GetBySlug(ctx context.Context, slug string) (*EntitySchema, error)
Update(ctx context.Context, id uuid.UUID, input *UpdateSchemaInput) (*EntitySchema, error)
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context) ([]*EntitySchema, error)
CountEntities(ctx context.Context, schemaID uuid.UUID) (int, error)
}
SchemaRepository defines the interface for entity schema data operations.
type SchemaSitemapSettings ¶
type SchemaSitemapSettings struct {
Priority *float64 `json:"priority,omitempty"`
ChangeFreq string `json:"changefreq,omitempty"`
Exclude bool `json:"exclude,omitempty"`
}
SchemaSitemapSettings tunes sitemap output for a schema.
type SchemaStatusCount ¶
type SchemaStatusCount struct {
SchemaID uuid.UUID
Status EntityStatus
Count int
}
SchemaStatusCount is how many entities of one schema have one status.
type SchemaURLConfig ¶
type SchemaURLConfig struct {
SchemaSlug string
Name string
// Aliases are extra schema slugs resolving to this schema.
Aliases []string
Pattern *urlpattern.Pattern
ArchivePath string
// SitemapGroup is the sitemap group the schema's entries belong to.
SitemapGroup string
Overrides []SchemaURLOverride
Sitemap SchemaSitemapSettings
SectionPages bool
// ArchiveTitle and ArchiveDescription are the trimmed settings values;
// empty when unset.
ArchiveTitle string
ArchiveDescription string
// ArchiveCrumbLabel is the trimmed archive_crumb_label setting.
ArchiveCrumbLabel string
// Explicit is true when the schema's settings supplied url_pattern.
Explicit bool
}
SchemaURLConfig is the effective routing configuration of one schema (settings merged over the built-in defaults).
type SchemaURLOverride ¶
type SchemaURLOverride struct {
Attribute string `json:"attribute"`
Equals string `json:"equals"`
URLPattern string `json:"url_pattern"`
SitemapGroup string `json:"sitemap_group,omitempty"`
}
SchemaURLOverride swaps the URL pattern (and sitemap group) for entities whose attribute `Attribute` equals `Equals`.
type SchemaURLSettings ¶
type SchemaURLSettings struct {
URLPattern string `json:"url_pattern,omitempty"`
ArchivePath string `json:"archive_path,omitempty"`
SitemapGroup string `json:"sitemap_group,omitempty"`
Aliases []string `json:"aliases,omitempty"`
Overrides []SchemaURLOverride `json:"overrides,omitempty"`
Sitemap SchemaSitemapSettings `json:"sitemap"`
SectionPages bool `json:"section_pages,omitempty"`
// ArchiveTitle and ArchiveDescription override the archive page heading
// and meta description (see MaxArchiveTitleLen / MaxArchiveDescriptionLen).
ArchiveTitle string `json:"archive_title,omitempty"`
ArchiveDescription string `json:"archive_description,omitempty"`
// ArchiveCrumbLabel is the section label in an entity's breadcrumb; it
// falls back to ArchiveTitle, then the schema name.
ArchiveCrumbLabel string `json:"archive_crumb_label,omitempty"`
// SchemaOrgType is the JSON-LD @type for entities of schemas that have no
// x-schema-org annotations and no plugin builder. Must be one of
// AllowedSchemaOrgTypes (checked on write only). Types that require
// fields the generic document cannot supply (FAQPage, Product, Event) are
// excluded; use annotations or a plugin builder for those.
SchemaOrgType string `json:"schema_org_type,omitempty"`
// Editor selects the admin editing screen for the type: "" is the generic
// form, EditorPost is the post (TipTap) editor used by blog posts.
// Any other value, including a non-string, reads as "" (checked on write only).
Editor EditorSetting `json:"editor,omitempty"`
}
SchemaURLSettings is the typed view of EntitySchema.Settings. All keys are optional; unknown keys are ignored so a site may carry extra data.
type SeriesRow ¶
type SeriesRow struct {
ID, Name, Slug string
Status EntityStatus
Attributes []byte
PublishedAt *time.Time
}
SeriesRow is one post of an editorial series, identity and timing only.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service provides business logic for entity management.
func NewService ¶
func NewService(schemaRepo SchemaRepository, entityRepo EntityRepository, logger *slog.Logger) *Service
NewService creates a new entity service.
func (*Service) ComputeDiff ¶
func (s *Service) ComputeDiff(ctx context.Context, entityID uuid.UUID, fromVersion, toVersion int) ([]VersionDiff, error)
ComputeDiff computes the differences between two versions.
func (*Service) CountEntitiesByParent ¶
CountEntitiesByParent returns the limit parents with the most children, with their counts.
func (*Service) CountEntitiesBySchemaStatus ¶
func (s *Service) CountEntitiesBySchemaStatus(ctx context.Context) ([]SchemaStatusCount, error)
CountEntitiesBySchemaStatus counts entities per schema and status in one query.
func (*Service) CountEntityChildren ¶
func (s *Service) CountEntityChildren(ctx context.Context, parentIDs []uuid.UUID) (map[uuid.UUID]int, error)
CountEntityChildren returns the direct child count of each of parentIDs (absent = none).
func (*Service) CreateEntity ¶
func (s *Service) CreateEntity(ctx context.Context, userID *uuid.UUID, input *CreateEntityInput) (*Entity, error)
CreateEntity creates a new entity.
func (*Service) CreateSchema ¶
func (s *Service) CreateSchema(ctx context.Context, input *CreateSchemaInput) (*EntitySchema, error)
CreateSchema creates a new entity schema.
func (*Service) DeleteEntity ¶
DeleteEntity removes an entity.
func (*Service) DeleteSchema ¶
DeleteSchema removes an entity schema.
func (*Service) GetEntityByParentSlug ¶
func (s *Service) GetEntityByParentSlug(ctx context.Context, parent *uuid.UUID, slug, schemaSlug string) (*Entity, error)
GetEntityByParentSlug retrieves the row with slug directly under parent (a root row when parent is nil). Slugs are unique per schema, not among all siblings: with schemaSlug the lookup is exact; without it, rows of several schemas sharing the slug give ErrAmbiguousSlug rather than a guess.
func (*Service) GetEntityBySlug ¶
GetEntityBySlug retrieves an entity by slug.
func (*Service) GetSchemaBySlug ¶
GetSchemaBySlug retrieves an entity schema by slug.
func (*Service) GetVersion ¶
func (s *Service) GetVersion(ctx context.Context, entityID uuid.UUID, version int) (*EntityVersion, error)
GetVersion retrieves a specific version of an entity.
func (*Service) Import ¶
func (s *Service) Import(ctx context.Context, actor *uuid.UUID, batch ImportBatch) (*ImportResult, error)
Import stores a batch of entities without any per-entity event: it sets dates, status, parent and author, validates attributes against the schema, sanitises them like a normal save, and emits ONE import.completed event for the whole batch (never entity.created / entity.published). Items fail individually (see ImportResult); the call itself fails only for an empty or oversized batch, or a repository without import support.
func (*Service) ListEntities ¶
func (s *Service) ListEntities(ctx context.Context, filter *ListEntitiesFilter, page *KeysetPage) (*PagedResult[*Entity], error)
ListEntities retrieves entities with filtering and keyset pagination.
func (*Service) ListEntitiesWithOffset ¶
func (s *Service) ListEntitiesWithOffset(ctx context.Context, filter *ListEntitiesFilter, page *OffsetPage) (*OffsetPagedResult[*Entity], error)
ListEntitiesWithOffset retrieves entities with filtering and offset pagination.
func (*Service) ListSchemas ¶
func (s *Service) ListSchemas(ctx context.Context) ([]*EntitySchema, error)
ListSchemas retrieves all entity schemas.
func (*Service) ListVersions ¶
func (s *Service) ListVersions(ctx context.Context, entityID uuid.UUID, limit int) ([]*EntityVersion, error)
ListVersions retrieves version history for an entity.
func (*Service) RefUsers ¶
func (s *Service) RefUsers(ctx context.Context, blockID uuid.UUID, limit int) (users []RefUser, capped bool, err error)
RefUsers lists the entities whose page_doc holds a ref node to blockID, newest first, at most limit (MaxRefUsers when limit <= 0). capped is true when more exist. The block itself is never listed, and neither is an entity that only carries the uuid as text.
func (*Service) RestoreVersion ¶
func (s *Service) RestoreVersion(ctx context.Context, entityID uuid.UUID, userID *uuid.UUID, version int) (*Entity, error)
RestoreVersion restores an entity to a previous version.
func (*Service) ReuseStatus ¶
func (s *Service) ReuseStatus() ReuseStatus
ReuseStatus reports which reuse types are available.
func (*Service) ScheduleEntity ¶
func (s *Service) ScheduleEntity(ctx context.Context, id uuid.UUID, userID *uuid.UUID, at time.Time) (*Entity, error)
ScheduleEntity sets the entity to publish automatically at the given time.
func (*Service) SetBeforeSave ¶
func (s *Service) SetBeforeSave(f BeforeSaveFunc)
SetBeforeSave installs the before_save hook; nil disables it.
func (*Service) SetBylines ¶
func (s *Service) SetBylines(b BylineResolver)
SetBylines installs the people resolver; without one bylines stay plain text.
func (*Service) SetEvents ¶
SetEvents installs the entity event sink. Safe to call before any request flows through the service; nil disables emission.
func (*Service) SetLinkSync ¶
func (s *Service) SetLinkSync(sync EntityLinkSync)
SetLinkSync installs the v2 linking sync hook. Called by the server during dependency wiring (server.go) after both the entity service and the v2 package are constructed. Safe to call before any request flows through UpdateEntity.
func (*Service) SetLiveObserver ¶
SetLiveObserver installs the callback run after an entity was created, published or imported; nil disables it.
func (*Service) SetOnSchemaChange ¶
func (s *Service) SetOnSchemaChange(fn func())
SetOnSchemaChange registers a callback run after a schema is created, updated or deleted (after the URL registry was invalidated), for example to purge caches derived from schema settings.
func (*Service) SetPathObserver ¶
func (s *Service) SetPathObserver(f PathChangeFunc)
SetPathObserver installs the callback run after an update changed an entity's slug or parent; nil disables it.
func (*Service) SetReuseStatus ¶
func (s *Service) SetReuseStatus(st ReuseStatus)
SetReuseStatus records what EnsureReuseSchemas found. It is called once at boot, before requests are served.
func (*Service) SetSchemaPatternObserver ¶
func (s *Service) SetSchemaPatternObserver(f SchemaPatternFunc)
SetSchemaPatternObserver installs the callback run after a schema update changed its URL pattern or overrides; nil disables it.
func (*Service) URLRegistry ¶
func (s *Service) URLRegistry() *URLRegistry
URLRegistry exposes the schema URL registry. It is invalidated by CreateSchema, UpdateSchema and DeleteSchema.
func (*Service) UpdateEntity ¶
func (s *Service) UpdateEntity(ctx context.Context, id uuid.UUID, userID *uuid.UUID, input *UpdateEntityInput) (*Entity, error)
UpdateEntity modifies an entity.
func (*Service) UpdateEntityStatus ¶
func (s *Service) UpdateEntityStatus(ctx context.Context, id uuid.UUID, userID *uuid.UUID, status EntityStatus) (*Entity, error)
UpdateEntityStatus changes an entity's status.
func (*Service) UpdateSchema ¶
func (s *Service) UpdateSchema(ctx context.Context, id uuid.UUID, input *UpdateSchemaInput) (*EntitySchema, error)
UpdateSchema modifies an entity schema.
func (*Service) ValidateEntityAttributes ¶
func (s *Service) ValidateEntityAttributes(ctx context.Context, schemaID uuid.UUID, attributes json.RawMessage) *ValidationResult
ValidateEntityAttributes validates attributes against a schema without creating an entity.
type SettingsError ¶
type SettingsError struct{ Field, Msg string }
SettingsError is a schema settings value that is not acceptable. Field names the setting at fault ("url_pattern", "archive_path", "aliases", ...; empty when the settings as a whole are malformed), so callers need not parse the message.
func (*SettingsError) Error ¶
func (e *SettingsError) Error() string
func (*SettingsError) Unwrap ¶
func (e *SettingsError) Unwrap() error
type SitemapEntry ¶
type SitemapEntry struct {
// ID is the entity row id (not serialised); the paths endpoint matches it
// against the configured homepage.
ID uuid.UUID `json:"-"`
Slug string `json:"slug"`
SchemaSlug string `json:"schema_slug"`
UpdatedAt string `json:"updated_at"`
Priority float64 `json:"priority"`
ChangeFreq string `json:"change_freq"`
NoIndex bool `json:"no_index"`
IncludeInLLMs bool `json:"include_in_llms"`
// EntityFormat is the value of attributes.format, used by the sitemap
// generator to route playbook entities with format='online_report' to
// /reports/{slug} instead of /playbooks/{slug} — reports have their
// own canonical URL family even though they share the playbook schema.
// Empty string for entities where attributes.format is unset or not a
// string. Only playbook-schema entities use this today.
EntityFormat string `json:"entity_format,omitempty"`
// ParentSlug is the slug of the entity's parent ("" for roots); it fills
// {parent} in URL patterns.
ParentSlug string `json:"parent_slug,omitempty"`
// AncestorPath is the slash-joined slugs of every ancestor, root first
// ("" for roots); it fills {path}. ParentSlug is its last segment.
AncestorPath string `json:"ancestor_path,omitempty"`
// PublishedAt (created_at when unset) fills {year}, {month} and {day}.
PublishedAt time.Time `json:"-"`
// Attrs holds the top-level attributes requested through
// ListAllPublishedWithAttrs, flattened to strings.
Attrs map[string]string `json:"-"`
}
SitemapEntry represents an entry in the sitemap.
type URLRegistry ¶
type URLRegistry struct {
// contains filtered or unexported fields
}
URLRegistry loads schema routing settings, caches the resulting router and is invalidated whenever a schema is created, updated or deleted. A short TTL bounds staleness for changes made by other processes.
func NewURLRegistry ¶
func NewURLRegistry(lister SchemaLister, logger *slog.Logger) *URLRegistry
NewURLRegistry creates a registry over lister. logger may be nil.
func (*URLRegistry) Invalidate ¶
func (r *URLRegistry) Invalidate()
Invalidate drops the cached snapshot; the next call reloads.
func (*URLRegistry) SetTimezone ¶
func (r *URLRegistry) SetTimezone(fn func(context.Context) string)
SetTimezone installs the source of the site time zone used for date URL tokens. It takes effect when the snapshot next reloads (at most urlRegistryTTL), or at once after Invalidate.
func (*URLRegistry) Snapshot ¶
func (r *URLRegistry) Snapshot(ctx context.Context) (*URLSnapshot, error)
Snapshot returns the cached snapshot, reloading when missing or expired.
type URLSnapshot ¶
type URLSnapshot struct {
// contains filtered or unexported fields
}
URLSnapshot is an immutable view of every schema's routing configuration.
Locale: a snapshot resolves paths without a language. Entities carry a locale, but only those in the site language are public, and URLs never contain the locale. A second public locale would need the changes listed in the urlpattern package doc (a path token, Resolve returning the locale, a per-locale filter, hreflang, per-locale sitemaps and feeds, translation links, an admin switcher); none of it is built.
func BuildURLSnapshot ¶
func BuildURLSnapshot(schemas []*EntitySchema, logger *slog.Logger) (*URLSnapshot, error)
BuildURLSnapshot merges schema settings over the defaults and builds the router. A schema whose stored settings are invalid is logged and routed with the defaults so one bad row cannot take the public site down.
func DefaultURLSnapshot ¶
func DefaultURLSnapshot() *URLSnapshot
DefaultURLSnapshot is the routing of an install without schema settings: page-like schemas at /{slug}, the post family at /blog/{slug}.
func (*URLSnapshot) BuildURL ¶
func (s *URLSnapshot) BuildURL(schemaSlug string, attrs map[string]string, v urlpattern.Vars) (string, bool, error)
BuildURL makes the snapshot usable as a linking URLBuilder: it renders the path for an entity of schemaSlug (own slug or alias). Unlike EntityPath it never falls back, so callers can tell an unknown schema from a real URL.
func (*URLSnapshot) Config ¶
func (s *URLSnapshot) Config(schemaSlug string) (*SchemaURLConfig, bool)
Config returns the configuration for a schema slug or alias.
func (*URLSnapshot) Configs ¶
func (s *URLSnapshot) Configs() []*SchemaURLConfig
Configs returns every schema's configuration ordered by schema slug.
func (*URLSnapshot) EntityPath ¶
func (s *URLSnapshot) EntityPath(ent *Entity, ancestors []string) string
EntityPath is Locate for a loaded entity whose ancestor slugs (root first) are given. It falls back to "/{slug}" (the pre-pattern behaviour) when the schema is unknown or the slug is empty.
func (*URLSnapshot) IsSectionPage ¶
func (s *URLSnapshot) IsSectionPage(schemaSlug, slug string) bool
IsSectionPage reports whether an entity of schemaSlug with the given slug is a section landing page: its schema has SectionPages and the slug equals a section prefix.
func (*URLSnapshot) Locate ¶
func (s *URLSnapshot) Locate(schemaSlug string, attrs map[string]string, v urlpattern.Vars) (loc EntityLocation, found bool, err error)
Locate resolves the public path of an entity of schemaSlug (own slug or alias) from its URL variables. Precedence: section pages, then explicit schema settings (including a matching override), then a plugin-registered EntityURLBuilder, then the built-in defaults. found is false when nothing knows the schema; err is a urlpattern error (for example ErrMissingVar for an empty slug).
func (*URLSnapshot) Location ¶
func (s *URLSnapshot) Location() *time.Location
Location is the time zone date URL tokens are rendered in.
func (*URLSnapshot) NeedsAncestors ¶
func (s *URLSnapshot) NeedsAncestors(schemaSlug string, attrs map[string]string) bool
NeedsAncestors reports whether the pattern serving an entity of schemaSlug with attrs uses {parent} or {path}, i.e. whether the caller should load the entity's ancestor slugs.
func (*URLSnapshot) NeedsEntityVars ¶
func (s *URLSnapshot) NeedsEntityVars(schemaSlug string) bool
NeedsEntityVars reports whether the pattern serving schemaSlug (base pattern or any override) uses {parent}, {path} or a date token, i.e. whether a URL can only be built from the loaded entity and not from its slug alone.
func (*URLSnapshot) OverrideAttributes ¶
func (s *URLSnapshot) OverrideAttributes() []string
OverrideAttributes returns the attribute names any schema override compares against (sorted, unique), i.e. the attributes a bulk listing must carry to pick each entity's pattern.
func (*URLSnapshot) PostArchivePath ¶
func (s *URLSnapshot) PostArchivePath() string
PostArchivePath is the post archive path of the site ("/blog" by default).
func (*URLSnapshot) PostPath ¶
func (s *URLSnapshot) PostPath(slug string) string
PostPath is the canonical path of the post with the given slug.
func (*URLSnapshot) Router ¶
func (s *URLSnapshot) Router() *urlpattern.Router
Router returns the pattern router built from the schemas.
func (*URLSnapshot) SectionPageSchemas ¶
func (s *URLSnapshot) SectionPageSchemas() []string
SectionPageSchemas returns the schema slugs (own slug plus aliases) of every schema with SectionPages enabled.
func (*URLSnapshot) SectionPrefixes ¶
func (s *URLSnapshot) SectionPrefixes() map[string]bool
SectionPrefixes returns the first literal segment of every pattern (for example "post" for /post/{slug}). A page-like schema with SectionPages set serves a row whose slug equals such a prefix at /{slug}.
func (*URLSnapshot) VarsFor ¶
func (s *URLSnapshot) VarsFor(ent *Entity, ancestors []string) urlpattern.Vars
VarsFor builds the URL variables of ent. ancestors are the slugs of its parent chain, root first (nil for a root). The date tokens read published_at (created_at while it is unset, for drafts and previews) in the site time zone.
func (*URLSnapshot) VarsForSitemap ¶
func (s *URLSnapshot) VarsForSitemap(e *SitemapEntry) urlpattern.Vars
VarsForSitemap is VarsFor for a sitemap row.
type UpdateEntityInput ¶
type UpdateEntityInput struct {
Name *string `json:"name,omitempty"`
Slug *string `json:"slug,omitempty"`
Locale *string `json:"locale,omitempty"` // re-tags the entity; nil leaves it as stored
Attributes *json.RawMessage `json:"attributes,omitempty"`
Metadata *json.RawMessage `json:"metadata,omitempty"`
ChangeSummary *string `json:"change_summary,omitempty"`
// Autosave marks a background save. It is recorded with AutosaveSummary,
// and a run of autosaves by one person within AutosaveWindow keeps a single
// version (the state before the run), so history holds checkpoints, not
// every pause in typing. Explicit saves always create a version.
Autosave bool `json:"-"`
// ExpectedVersion, when set, makes the write conditional: the repository
// compares it with the stored version under its row lock and returns a
// *VersionConflictError instead of writing when they differ.
ExpectedVersion *int `json:"-"`
// KeepUpdatedAt leaves updated_at as it is (the version still advances).
// Internal: used by `builder convert`, so a lossless shape
// change does not look like a content edit (JSON-LD dateModified, sitemap lastmod).
KeepUpdatedAt bool `json:"-"`
// ParentID moves the entity under a parent; ClearParent makes it a root.
// Both unset leaves the parent alone. The same holds for AuthorID.
ParentID *uuid.UUID `json:"parent_id,omitempty"`
ClearParent bool `json:"clear_parent,omitempty"`
AuthorID *uuid.UUID `json:"author_id,omitempty"`
ClearAuthor bool `json:"clear_author,omitempty"`
// BylinePersonID sets the credited person and ClearBylinePerson removes
// it; BylineOrg sets or clears the site-organisation byline. Both unset
// leaves the byline alone. The columns are independent: a caller that
// switches from a person to the organisation sets both.
BylinePersonID *uuid.UUID `json:"byline_person_id,omitempty"`
ClearBylinePerson bool `json:"clear_byline_person,omitempty"`
BylineOrg *bool `json:"byline_org,omitempty"`
// Status is set by the service for a person or a block/part (see lockedStatus); unset
// leaves the status alone.
Status *EntityStatus `json:"-"`
}
UpdateEntityInput represents input for updating an entity.
type UpdateSchemaInput ¶
type UpdateSchemaInput struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Schema *json.RawMessage `json:"schema,omitempty"`
Settings *json.RawMessage `json:"settings,omitempty"`
}
UpdateSchemaInput represents input for updating an entity schema.
type UpdateStatusInput ¶
type UpdateStatusInput struct {
Status EntityStatus `json:"status"`
}
UpdateStatusInput represents input for changing an entity's status.
type ValidationError ¶
type ValidationError struct {
Path string `json:"path"`
Message string `json:"message"`
// Keyword is the JSON Schema keyword that failed ("required", "maxLength",
// ...), set on the leaf errors; the admin forms map it to friendly text.
Keyword string `json:"keyword,omitempty"`
}
ValidationError represents a single validation error.
type ValidationResult ¶
type ValidationResult struct {
Valid bool `json:"valid"`
Errors []ValidationError `json:"errors,omitempty"`
}
ValidationResult holds detailed validation results.
type Validator ¶
type Validator struct {
// contains filtered or unexported fields
}
Validator validates entity attributes against JSON schemas.
func NewValidator ¶
func NewValidator() *Validator
NewValidator creates a new JSON Schema validator.
func (*Validator) ValidateAttributes ¶
func (v *Validator) ValidateAttributes(schemaJSON, attributesJSON json.RawMessage) error
ValidateAttributes validates entity attributes against a schema.
func (*Validator) ValidateSchema ¶
func (v *Validator) ValidateSchema(schemaJSON json.RawMessage) error
ValidateSchema validates that a JSON schema is itself valid.
It performs three checks in order:
- The bytes parse as JSON and the root has a `type`.
- The shape is something the Phase 3 visual builder can edit (ValidateSchemaShape) — bounded primitive set, every property typed, nested objects/arrays well-formed, no surprises that would crash the editor at render time.
- The schema compiles as JSON Schema Draft 2020-12 so the entity validator can later use it to check instances.
Any failure returns ErrInvalidSchema wrapped with a path-prefixed reason so the admin UI can surface "properties.foo.type missing" rather than a generic compile error.
func (*Validator) ValidateWithDetails ¶
func (v *Validator) ValidateWithDetails(schemaJSON, attributesJSON json.RawMessage) *ValidationResult
ValidateWithDetails validates attributes and returns detailed results.
type VersionConflictError ¶
type VersionConflictError struct{ Current int }
VersionConflictError is a conditional write (UpdateEntityInput.ExpectedVersion) that found another version stored. It matches ErrVersionConflict.
func (*VersionConflictError) Error ¶
func (e *VersionConflictError) Error() string
func (*VersionConflictError) Is ¶
func (e *VersionConflictError) Is(target error) bool
type VersionDiff ¶
type VersionDiff struct {
Field string `json:"field"`
OldValue any `json:"old_value"`
NewValue any `json:"new_value"`
}
VersionDiff represents a difference between two entity versions.
Source Files
¶
- aicomment.go
- builder_save.go
- builtin_schema.go
- events.go
- faq.go
- hierarchy.go
- import.go
- jsonld_generator.go
- jsonld_hooks.go
- locale.go
- model.go
- page_doc.go
- page_doc_derive.go
- plaintext.go
- public_listing.go
- public_repository.go
- repository.go
- repository_hierarchy.go
- reuse_schema.go
- reuse_usage.go
- sanitize.go
- sanitize_render.go
- sanitize_schema.go
- sanitize_sections.go
- schema_settings.go
- service.go
- url_builder_hooks.go
- url_paths.go
- url_registry.go
- validation.go