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>
189 lines
6.3 KiB
Go
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,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|