package main import ( "context" "github.com/a-h/templ" "git.dev.alexdunmow.com/block/core/blocks" "git.dev.alexdunmow.com/block/core/plugin" "git.dev.alexdunmow.com/block/core/templates" ) // wrap adapts a templ-returning render function to templates.TemplateFunc. // templ.Component already implements templates.HTMLComponent via its Render // method, so the adapter only has to swap the return type. func wrap(f func(ctx context.Context, doc map[string]any) templ.Component) templates.TemplateFunc { return func(ctx context.Context, doc map[string]any) templates.HTMLComponent { return f(ctx, doc) } } // Register is the plugin entry point. The call order matters: // 1. RegisterSystemTemplate seeds the system-template row. // 2. RegisterPageTemplate once per page template (default / landing / // article / full-width). // 3. br.LoadSchemasFromFS BEFORE any br.Register so editor-side schemas // are bound to their block keys. // 4. br.Register for each theme-owned block (registered unqualified; // addressed downstream as "editorial:"). // 5. br.RegisterTemplateOverride for the four built-ins editorial styles // differently (heading, text, button, image). The overrides are scoped // to the "editorial" template — other templates keep their built-ins. // 6. tr.RegisterEmailWrapper for the editorial-branded email layout. func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error { tr.RegisterSystemTemplate(templates.SystemTemplateMeta{ Key: "editorial", Title: "Editorial", Description: "Didone display, narrow column, hairline rules for the modern newsroom.", }) if err := tr.RegisterPageTemplate("editorial", templates.PageTemplateMeta{ Key: "default", Title: "Default", Description: "Standard page with masthead, narrow main column, footer", Slots: []string{"header", "main", "footer"}, }, wrap(RenderEditorial)); err != nil { return err } if err := tr.RegisterPageTemplate("editorial", templates.PageTemplateMeta{ Key: "landing", Title: "Section Front", Description: "Front-page-style stack: masthead, lead story, river, footer", Slots: []string{"masthead", "lead", "river", "footer"}, }, wrap(RenderEditorialLanding)); err != nil { return err } if err := tr.RegisterPageTemplate("editorial", templates.PageTemplateMeta{ Key: "article", Title: "Article", Description: "Single-column long read with byline, drop cap, marginalia rail", Slots: []string{"header", "byline", "main", "marginalia", "footer"}, }, wrap(RenderEditorialArticle)); err != nil { return err } if err := tr.RegisterPageTemplate("editorial", templates.PageTemplateMeta{ Key: "full-width", Title: "Full Width", Description: "Edge-to-edge for photo essays and data graphics", Slots: []string{"header", "main", "footer"}, }, wrap(RenderEditorialFullWidth)); err != nil { return err } // Schemas MUST be loaded before any Register so the editor binds the // correct content schema to each block at registration time. if err := br.LoadSchemasFromFS(Schemas()); err != nil { return err } // Theme-owned blocks. br.Register(MastheadBlockMeta, MastheadBlock) br.Register(BylineBlockMeta, BylineBlock) br.Register(PullquoteBlockMeta, PullquoteBlock) br.Register(DropcapIntroBlockMeta, DropcapIntroBlock) br.Register(MarginaliaBlockMeta, MarginaliaBlock) br.Register(SectionLabelBlockMeta, SectionLabelBlock) br.Register(ColophonBlockMeta, ColophonBlock) // Built-in overrides scoped to the editorial template. br.RegisterTemplateOverride("editorial", "heading", EditorialHeadingBlock) br.RegisterTemplateOverride("editorial", "text", EditorialTextBlock) br.RegisterTemplateOverride("editorial", "button", EditorialButtonBlock) br.RegisterTemplateOverride("editorial", "image", EditorialImageBlock) // Branded email wrapper. tr.RegisterEmailWrapper("editorial", EditorialEmailWrapper) return nil } // DefaultMasterPages returns the two master pages editorial seeds on first // load. The slot keys and BlockKey strings here match spec §7 exactly; the // `slot` block carries `slotName` equal to its rendered slot key so the CMS // knows where to inject page content. func DefaultMasterPages() []plugin.MasterPageDefinition { return []plugin.MasterPageDefinition{ { Key: "editorial:default-master", Title: "Editorial Default Master", PageTemplates: []string{"default", "landing", "full-width"}, Blocks: []plugin.MasterPageBlock{ { BlockKey: "editorial:masthead", Title: "Masthead", Content: map[string]any{"menuName": "main", "kicker": "EST. 2026"}, Slot: "header", SortOrder: 0, }, { BlockKey: "navbar", Title: "Section Nav", Content: map[string]any{"menuName": "sections"}, Slot: "header", SortOrder: 1, }, { BlockKey: "slot", Title: "Main Content", Content: map[string]any{"slotName": "main", "placeholder": "Page content"}, Slot: "main", SortOrder: 0, }, { BlockKey: "editorial:colophon", Title: "Colophon", Content: map[string]any{"showSignup": true, "issn": "0000-0000"}, Slot: "footer", SortOrder: 0, }, }, }, { Key: "editorial:article-master", Title: "Editorial Article Master", PageTemplates: []string{"article"}, Blocks: []plugin.MasterPageBlock{ { BlockKey: "editorial:masthead", Title: "Masthead", Content: map[string]any{"menuName": "main", "compact": true}, Slot: "header", SortOrder: 0, }, { BlockKey: "editorial:byline", Title: "Byline", Content: map[string]any{"showPhoto": true, "showReadTime": true}, Slot: "byline", SortOrder: 0, }, { BlockKey: "slot", Title: "Article Body", Content: map[string]any{"slotName": "main"}, Slot: "main", SortOrder: 0, }, { BlockKey: "editorial:marginalia", Title: "Marginalia Rail", Content: map[string]any{"items": []any{}}, Slot: "marginalia", SortOrder: 0, }, { BlockKey: "editorial:colophon", Title: "Colophon", Content: map[string]any{"showSignup": true}, Slot: "footer", SortOrder: 0, }, }, }, } }