feat(bnp): codeless system/page templates — layouts without code (WO-WZ-020)
manifest.yaml gains system_templates + page_templates declarations; each page template's .ninjatpl source lives at templates/<system>/<key>.ninjatpl and is validated at pack time. Verify no longer rejects declared system/page templates on codeless manifests (guest template_keys stay rejected) — the cms host registers them source-tracked and renders the files through the same host pongo pipeline powered blocks use. Blog/system/normal page layouts are now fully expressible with zero code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4a1d1883cb
commit
52d7413aa0
@ -39,6 +39,18 @@ type manifestYAML struct {
|
|||||||
SettingsSchema string `yaml:"settings_schema"` // JSON file
|
SettingsSchema string `yaml:"settings_schema"` // JSON file
|
||||||
MasterPages string `yaml:"master_pages"` // JSON file ([]masterPageJSON)
|
MasterPages string `yaml:"master_pages"` // JSON file ([]masterPageJSON)
|
||||||
RequiredIconPacks []string `yaml:"required_icon_packs"`
|
RequiredIconPacks []string `yaml:"required_icon_packs"`
|
||||||
|
SystemTemplates []struct {
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
Title string `yaml:"title"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
} `yaml:"system_templates"`
|
||||||
|
PageTemplates []struct {
|
||||||
|
System string `yaml:"system"`
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
Title string `yaml:"title"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Slots []string `yaml:"slots"`
|
||||||
|
} `yaml:"page_templates"`
|
||||||
CSS *struct {
|
CSS *struct {
|
||||||
NpmPackages map[string]string `yaml:"npm_packages"`
|
NpmPackages map[string]string `yaml:"npm_packages"`
|
||||||
CSSDirectives []string `yaml:"css_directives"`
|
CSSDirectives []string `yaml:"css_directives"`
|
||||||
@ -382,6 +394,30 @@ func applyManifestYAML(dir string, m *abiv1.PluginManifest) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, st := range my.SystemTemplates {
|
||||||
|
if st.Key == "" || st.Title == "" {
|
||||||
|
return fmt.Errorf("%s: system_templates entries need key and title", manifestYAMLName)
|
||||||
|
}
|
||||||
|
m.SystemTemplates = append(m.SystemTemplates, &abiv1.SystemTemplateMeta{
|
||||||
|
Key: st.Key, Title: st.Title, Description: st.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, pt := range my.PageTemplates {
|
||||||
|
if pt.System == "" || pt.Key == "" || pt.Title == "" {
|
||||||
|
return fmt.Errorf("%s: page_templates entries need system, key, and title", manifestYAMLName)
|
||||||
|
}
|
||||||
|
// Layout source convention: templates/<system>/<key>.ninjatpl,
|
||||||
|
// rendered host-side (core/docs/codeless-bnp.md).
|
||||||
|
rel := filepath.Join(dirTemplates, pt.System, pt.Key+".ninjatpl")
|
||||||
|
if err := fileWithin(dir, rel); err != nil {
|
||||||
|
return fmt.Errorf("%s: page template %s/%s: %w", manifestYAMLName, pt.System, pt.Key, err)
|
||||||
|
}
|
||||||
|
m.PageTemplates = append(m.PageTemplates, &abiv1.PageTemplateMeta{
|
||||||
|
SystemKey: pt.System, Key: pt.Key, Title: pt.Title,
|
||||||
|
Description: pt.Description, Slots: pt.Slots,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if my.MasterPages != "" {
|
if my.MasterPages != "" {
|
||||||
mpRaw, err := readJSONFile(my.MasterPages, "master_pages")
|
mpRaw, err := readJSONFile(my.MasterPages, "master_pages")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -29,7 +29,10 @@ func codelessFixture(t *testing.T) string {
|
|||||||
writeFixture(t, dir, map[string]string{
|
writeFixture(t, dir, map[string]string{
|
||||||
"plugin.mod": "[plugin]\nname = \"fixture-theme\"\nversion = \"0.1.0\"\nkind = \"theme\"\n",
|
"plugin.mod": "[plugin]\nname = \"fixture-theme\"\nversion = \"0.1.0\"\nkind = \"theme\"\n",
|
||||||
"manifest.yaml": "theme_presets: presets.json\nrequired_icon_packs: [lucide]\n" +
|
"manifest.yaml": "theme_presets: presets.json\nrequired_icon_packs: [lucide]\n" +
|
||||||
"master_pages: master_pages.json\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>`,
|
||||||
"presets.json": `{"presets":[{"key":"default"}]}`,
|
"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}]}]`,
|
"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" +
|
"blocks/blocks.yaml": "blocks:\n - key: hero\n title: Hero\n category: content\n" +
|
||||||
@ -73,6 +76,9 @@ func TestCodelessBuildAndVerifyRoundTrip(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if len(m.GetSystemTemplates()) != 1 || len(m.GetPageTemplates()) != 1 || m.GetPageTemplates()[0].GetSystemKey() != "fixture" {
|
||||||
|
t.Errorf("templates = %v / %v", m.GetSystemTemplates(), m.GetPageTemplates())
|
||||||
|
}
|
||||||
if !m.GetCodeless() || len(m.GetMasterPages()) != 1 || len(m.GetThemePresets()) == 0 {
|
if !m.GetCodeless() || len(m.GetMasterPages()) != 1 || len(m.GetThemePresets()) == 0 {
|
||||||
t.Errorf("manifest = codeless:%v masters:%d presets:%dB",
|
t.Errorf("manifest = codeless:%v masters:%d presets:%dB",
|
||||||
m.GetCodeless(), len(m.GetMasterPages()), len(m.GetThemePresets()))
|
m.GetCodeless(), len(m.GetMasterPages()), len(m.GetThemePresets()))
|
||||||
|
|||||||
@ -159,8 +159,8 @@ func CodelessHookViolation(m *abiv1.PluginManifest) error {
|
|||||||
return viol("template tags/filters (express pure snippets as template partials)")
|
return viol("template tags/filters (express pure snippets as template partials)")
|
||||||
case len(m.GetRbacMethodRoles()) > 0 || len(m.GetCoreServiceBindings()) > 0:
|
case len(m.GetRbacMethodRoles()) > 0 || len(m.GetCoreServiceBindings()) > 0:
|
||||||
return viol("Connect services")
|
return viol("Connect services")
|
||||||
case len(m.GetBlocks()) > 0 || len(m.GetTemplateKeys()) > 0 || len(m.GetSystemTemplates()) > 0 || len(m.GetPageTemplates()) > 0:
|
case len(m.GetBlocks()) > 0 || len(m.GetTemplateKeys()) > 0:
|
||||||
return viol("guest-rendered blocks/templates (codeless blocks live in blocks/blocks.yaml)")
|
return viol("guest-rendered blocks/template keys (codeless blocks live in blocks/blocks.yaml; layouts are declared system/page templates rendered host-side)")
|
||||||
case m.GetDirectoryExtensions().GetPanelSectionCount() > 0 || m.GetDirectoryExtensions().GetPinDecoratorCount() > 0:
|
case m.GetDirectoryExtensions().GetPanelSectionCount() > 0 || m.GetDirectoryExtensions().GetPinDecoratorCount() > 0:
|
||||||
return viol("directory extension callbacks")
|
return viol("directory extension callbacks")
|
||||||
case m.GetDataDir():
|
case m.GetDataDir():
|
||||||
|
|||||||
@ -48,7 +48,9 @@ my-theme/
|
|||||||
blocks.yaml # key/title/category/schema/template/providers per block
|
blocks.yaml # key/title/category/schema/template/providers per block
|
||||||
hero.schema.json
|
hero.schema.json
|
||||||
hero.ninjatpl
|
hero.ninjatpl
|
||||||
templates/ # reserved for host-rendered page templates (future)
|
templates/ # <system>/<key>.ninjatpl layout sources for the
|
||||||
|
# system/page templates declared in manifest.yaml,
|
||||||
|
# rendered host-side (blog/system/normal page layouts)
|
||||||
seed/
|
seed/
|
||||||
seed.json # settings {merge/override/ensure}, media, pages, menu_items
|
seed.json # settings {merge/override/ensure}, media, pages, menu_items
|
||||||
hero.jpg # media bytes referenced by seed.json entries
|
hero.jpg # media bytes referenced by seed.json entries
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user