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
|
||||
MasterPages string `yaml:"master_pages"` // JSON file ([]masterPageJSON)
|
||||
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 {
|
||||
NpmPackages map[string]string `yaml:"npm_packages"`
|
||||
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 != "" {
|
||||
mpRaw, err := readJSONFile(my.MasterPages, "master_pages")
|
||||
if err != nil {
|
||||
|
||||
@ -29,7 +29,10 @@ func codelessFixture(t *testing.T) string {
|
||||
writeFixture(t, dir, map[string]string{
|
||||
"plugin.mod": "[plugin]\nname = \"fixture-theme\"\nversion = \"0.1.0\"\nkind = \"theme\"\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"}]}`,
|
||||
"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" +
|
||||
@ -73,6 +76,9 @@ func TestCodelessBuildAndVerifyRoundTrip(t *testing.T) {
|
||||
if err != nil {
|
||||
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 {
|
||||
t.Errorf("manifest = codeless:%v masters:%d presets:%dB",
|
||||
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)")
|
||||
case len(m.GetRbacMethodRoles()) > 0 || len(m.GetCoreServiceBindings()) > 0:
|
||||
return viol("Connect services")
|
||||
case len(m.GetBlocks()) > 0 || len(m.GetTemplateKeys()) > 0 || len(m.GetSystemTemplates()) > 0 || len(m.GetPageTemplates()) > 0:
|
||||
return viol("guest-rendered blocks/templates (codeless blocks live in blocks/blocks.yaml)")
|
||||
case len(m.GetBlocks()) > 0 || len(m.GetTemplateKeys()) > 0:
|
||||
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:
|
||||
return viol("directory extension callbacks")
|
||||
case m.GetDataDir():
|
||||
|
||||
@ -48,7 +48,9 @@ my-theme/
|
||||
blocks.yaml # key/title/category/schema/template/providers per block
|
||||
hero.schema.json
|
||||
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.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