feat(bnp): codeless template overrides + email wrappers (WO-WZ-021)
manifest.yaml gains template_overrides [{template, block}] and
email_wrappers [system keys]; sources by convention at
templates/overrides/<template>/<block>.ninjatpl and
templates/email/<system>.ninjatpl (validated at build). Populates the
existing manifest fields BlockTemplateOverrides / EmailWrapperSystemKeys —
no proto change. Unblocks full theme-catalog codeless conversion: every
theme in the fleet registers overrides + an email wrapper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8bd92ea5c4
commit
87bbf9fe46
@ -51,7 +51,19 @@ type manifestYAML struct {
|
||||
Description string `yaml:"description"`
|
||||
Slots []string `yaml:"slots"`
|
||||
} `yaml:"page_templates"`
|
||||
CSS *struct {
|
||||
// TemplateOverrides declare theme-scoped overrides of builtin block
|
||||
// rendering. Source convention: templates/overrides/<template>/<block>.ninjatpl,
|
||||
// rendered host-side with the block's content map as the template context.
|
||||
TemplateOverrides []struct {
|
||||
Template string `yaml:"template"` // template/system key the override binds to
|
||||
Block string `yaml:"block"` // builtin block key being overridden
|
||||
} `yaml:"template_overrides"`
|
||||
// EmailWrappers declare branded email wrappers per system key. Source
|
||||
// convention: templates/email/<system>.ninjatpl, rendered host-side with
|
||||
// body/colors/site/unsubscribe_url/preview_text in the context (emit the
|
||||
// pre-rendered body with |safe).
|
||||
EmailWrappers []string `yaml:"email_wrappers"`
|
||||
CSS *struct {
|
||||
NpmPackages map[string]string `yaml:"npm_packages"`
|
||||
CSSDirectives []string `yaml:"css_directives"`
|
||||
InputCSSAppend string `yaml:"input_css_append"`
|
||||
@ -418,6 +430,29 @@ func applyManifestYAML(dir string, m *abiv1.PluginManifest) error {
|
||||
})
|
||||
}
|
||||
|
||||
for _, to := range my.TemplateOverrides {
|
||||
if to.Template == "" || to.Block == "" {
|
||||
return fmt.Errorf("%s: template_overrides entries need template and block", manifestYAMLName)
|
||||
}
|
||||
rel := filepath.Join(dirTemplates, "overrides", to.Template, to.Block+".ninjatpl")
|
||||
if err := fileWithin(dir, rel); err != nil {
|
||||
return fmt.Errorf("%s: template override %s/%s: %w", manifestYAMLName, to.Template, to.Block, err)
|
||||
}
|
||||
m.BlockTemplateOverrides = append(m.BlockTemplateOverrides, &abiv1.BlockTemplateOverride{
|
||||
TemplateKey: to.Template, BlockKey: to.Block,
|
||||
})
|
||||
}
|
||||
for _, ek := range my.EmailWrappers {
|
||||
if ek == "" {
|
||||
return fmt.Errorf("%s: email_wrappers entries must be non-empty system keys", manifestYAMLName)
|
||||
}
|
||||
rel := filepath.Join(dirTemplates, "email", ek+".ninjatpl")
|
||||
if err := fileWithin(dir, rel); err != nil {
|
||||
return fmt.Errorf("%s: email wrapper %s: %w", manifestYAMLName, ek, err)
|
||||
}
|
||||
m.EmailWrapperSystemKeys = append(m.EmailWrapperSystemKeys, ek)
|
||||
}
|
||||
|
||||
if my.MasterPages != "" {
|
||||
mpRaw, err := readJSONFile(my.MasterPages, "master_pages")
|
||||
if err != nil {
|
||||
|
||||
@ -31,8 +31,12 @@ func codelessFixture(t *testing.T) string {
|
||||
"manifest.yaml": "theme_presets: presets.json\nrequired_icon_packs: [lucide]\n" +
|
||||
"master_pages: master_pages.json\n" +
|
||||
"system_templates:\n - key: fixture\n title: Fixture Theme\n" +
|
||||
"page_templates:\n - system: fixture\n key: landing\n title: Landing\n slots: [main]\n",
|
||||
"templates/fixture/landing.ninjatpl": `<main>{{ content }}</main>`,
|
||||
"page_templates:\n - system: fixture\n key: landing\n title: Landing\n slots: [main]\n" +
|
||||
"template_overrides:\n - template: fixture\n block: heading\n" +
|
||||
"email_wrappers: [fixture]\n",
|
||||
"templates/fixture/landing.ninjatpl": `<main>{{ content }}</main>`,
|
||||
"templates/overrides/fixture/heading.ninjatpl": `<h2 class="deco">{{ text }}</h2>`,
|
||||
"templates/email/fixture.ninjatpl": `<table><tr><td>{{ body|safe }}</td></tr></table>`,
|
||||
"presets.json": `{"presets":[{"key":"default"}]}`,
|
||||
"master_pages.json": `[{"key":"landing","title":"Landing","blocks":[{"block_key":"html","title":"Hero","content":{"x":1},"slot":"main","sort_order":1}]}]`,
|
||||
"blocks/blocks.yaml": "blocks:\n - key: hero\n title: Hero\n category: content\n" +
|
||||
@ -86,6 +90,14 @@ func TestCodelessBuildAndVerifyRoundTrip(t *testing.T) {
|
||||
if len(m.GetRequiredIconPacks()) != 1 || m.GetRequiredIconPacks()[0] != "lucide" {
|
||||
t.Errorf("icon packs = %v", m.GetRequiredIconPacks())
|
||||
}
|
||||
if len(m.GetBlockTemplateOverrides()) != 1 ||
|
||||
m.GetBlockTemplateOverrides()[0].GetTemplateKey() != "fixture" ||
|
||||
m.GetBlockTemplateOverrides()[0].GetBlockKey() != "heading" {
|
||||
t.Errorf("template overrides = %v", m.GetBlockTemplateOverrides())
|
||||
}
|
||||
if len(m.GetEmailWrapperSystemKeys()) != 1 || m.GetEmailWrapperSystemKeys()[0] != "fixture" {
|
||||
t.Errorf("email wrappers = %v", m.GetEmailWrapperSystemKeys())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCodelessRepoFalseWithGoSource(t *testing.T) {
|
||||
@ -123,6 +135,19 @@ func TestCodelessBuildRejectsBadDeclarations(t *testing.T) {
|
||||
{"data_dir grant", map[string]string{
|
||||
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\ndata_dir = true\n",
|
||||
}},
|
||||
{"override missing template file", map[string]string{
|
||||
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\n",
|
||||
"manifest.yaml": "template_overrides:\n - template: x\n block: heading\n",
|
||||
}},
|
||||
{"override missing block key", map[string]string{
|
||||
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\n",
|
||||
"manifest.yaml": "template_overrides:\n - template: x\n",
|
||||
"templates/overrides/x/heading.ninjatpl": `<h1>{{ text }}</h1>`,
|
||||
}},
|
||||
{"email wrapper missing template file", map[string]string{
|
||||
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\n",
|
||||
"manifest.yaml": "email_wrappers: [x]\n",
|
||||
}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
@ -28,6 +28,19 @@ plugin is *data the host runs*, not a program that links a library. No Go, no
|
||||
- Master pages, theme presets, fonts, CSS manifest, icon packs, settings
|
||||
schema (`manifest.yaml`), assets (host-served), migrations (host-run Goose),
|
||||
seed data (`seed/seed.json`, applied via the WO-WZ-019 provisioner).
|
||||
- **Template overrides** (WO-WZ-021): theme-scoped re-renders of builtin
|
||||
blocks. Declared in `manifest.yaml` as
|
||||
`template_overrides: [{template: <system-key>, block: <block-key>}]`; the
|
||||
source lives at `templates/overrides/<template>/<block>.ninjatpl` and is
|
||||
rendered host-side with the block's content map as the template context.
|
||||
- **Email wrappers** (WO-WZ-021): `email_wrappers: [<system-key>, …]` in
|
||||
`manifest.yaml`, source at `templates/email/<system>.ninjatpl`. The context
|
||||
carries `body` (pre-rendered HTML — emit it with `{{ body|safe }}`),
|
||||
`colors.*` (snake_case hex tokens: `primary`, `primary_foreground`,
|
||||
`secondary`, `secondary_foreground`, `background`, `foreground`, `muted`,
|
||||
`muted_foreground`, `border`, `card`, `card_foreground`), `site.*` (`name`,
|
||||
`logo_url`, `url`, `support_email`), `unsubscribe_url`, `preview_text`.
|
||||
Render failure falls back to the unwrapped body.
|
||||
|
||||
**Requires code (a wasm plugin):**
|
||||
- A block whose data no declared provider can produce; a *computing* tag or
|
||||
@ -43,7 +56,10 @@ my-theme/
|
||||
plugin.mod # name/version/kind (data_dir forbidden)
|
||||
manifest.yaml # optional: theme_presets/bundled_fonts/settings_schema
|
||||
# (JSON file refs), master_pages (JSON file),
|
||||
# required_icon_packs, css {...}, dependencies [...]
|
||||
# required_icon_packs, css {...}, dependencies [...],
|
||||
# system_templates/page_templates,
|
||||
# template_overrides [{template, block}],
|
||||
# email_wrappers [system keys]
|
||||
blocks/
|
||||
blocks.yaml # key/title/category/schema/template/providers per block
|
||||
hero.schema.json
|
||||
@ -51,6 +67,8 @@ my-theme/
|
||||
templates/ # <system>/<key>.ninjatpl layout sources for the
|
||||
# system/page templates declared in manifest.yaml,
|
||||
# rendered host-side (blog/system/normal page layouts)
|
||||
overrides/<system>/<block>.ninjatpl # template_overrides sources
|
||||
email/<system>.ninjatpl # email_wrappers sources
|
||||
seed/
|
||||
seed.json # settings {merge/override/ensure}, media, pages, menu_items
|
||||
hero.jpg # media bytes referenced by seed.json entries
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user