diff --git a/cmd/ninja/internal/bnp/codeless.go b/cmd/ninja/internal/bnp/codeless.go index 5386e1a..415281e 100644 --- a/cmd/ninja/internal/bnp/codeless.go +++ b/cmd/ninja/internal/bnp/codeless.go @@ -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//.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 { diff --git a/cmd/ninja/internal/bnp/codeless_test.go b/cmd/ninja/internal/bnp/codeless_test.go index e17e404..68ea19e 100644 --- a/cmd/ninja/internal/bnp/codeless_test.go +++ b/cmd/ninja/internal/bnp/codeless_test.go @@ -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": `
{{ content }}
`, "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())) diff --git a/cmd/ninja/internal/bnp/verify.go b/cmd/ninja/internal/bnp/verify.go index e3f4e53..ccbd4bb 100644 --- a/cmd/ninja/internal/bnp/verify.go +++ b/cmd/ninja/internal/bnp/verify.go @@ -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(): diff --git a/docs/codeless-bnp.md b/docs/codeless-bnp.md index ca94810..4c0819f 100644 --- a/docs/codeless-bnp.md +++ b/docs/codeless-bnp.md @@ -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/ # /.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