themes-editorial/register.go
Alex Dunmow 1d9a4c8ce6 initial: theme plugin editorial
Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously
an unversioned directory inside ~/src/blockninja-themes/editorial.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 14:11:28 +08:00

189 lines
6.3 KiB
Go

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:<key>").
// 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,
},
},
},
}
}