`)
+ assert.NotContains(t, aboutHTML, `pp-description pp-content-page-body`)
+ assert.Contains(t, aboutHTML, `href="guides/setup.html#install"`)
+ assert.Contains(t, aboutHTML, `src="assets/docs/about/diagram.svg"`)
+ assert.Contains(t, aboutHTML, "Team onboarding notes.")
+ guideHTML := readFile(t, filepath.Join(outputDir, "guides", "setup.html"))
+ assert.Contains(t, guideHTML, `href="../auth.html"`)
+ assert.Contains(t, guideHTML, `src="../assets/docs/guides/setup/diagram.svg"`)
+ guidesHTML := readFile(t, filepath.Join(outputDir, "guides.html"))
+ assert.Contains(t, guidesHTML, `
Guides
`)
+ assert.Contains(t, guidesHTML, `
GUIDES`)
+ assert.Contains(t, guidesHTML, `href="about.html"`)
+ assert.Contains(t, guidesHTML, `href="guides/setup.html"`)
+ assert.Contains(t, guidesHTML, `href="webhooks-guide.html"`)
+ assert.NotContains(t, guidesHTML, `href="faq.html"`)
+
+ _, err = pp.PrintLLM()
+ require.NoError(t, err)
+ llmFull := readFile(t, filepath.Join(outputDir, "llms-full.txt"))
+ assert.NotContains(t, llmFull, "Root content unique to custom docs.")
+ assert.NotContains(t, llmFull, "Webhooks guide content.")
+}
+
+func TestContentPages_RequireExplicitEnable(t *testing.T) {
+ root := t.TempDir()
+ specPath := filepath.Join(root, "openapi.yaml")
+ writeFile(t, specPath, minimalContentPageSpec())
+ writeFile(t, filepath.Join(root, "about.md"), "Internal notes should not publish by default.\n")
+
+ specBytes, err := os.ReadFile(specPath)
+ require.NoError(t, err)
+ pp, err := CreatePrintingPressFromBytes(specBytes, &PrintingPressConfig{
+ BasePath: root,
+ SpecPath: specPath,
+ })
+ require.NoError(t, err)
+
+ site, err := pp.PressModel()
+ require.NoError(t, err)
+ assert.Empty(t, site.ContentPages)
+}
+
+func TestContentPages_ImageBasenameCollisionsReuseOrDisambiguate(t *testing.T) {
+ restoreLogger := silenceDefaultLogger()
+ defer restoreLogger()
+
+ root := t.TempDir()
+ specPath := filepath.Join(root, "openapi.yaml")
+ writeFile(t, specPath, minimalContentPageSpec())
+ writeFile(t, filepath.Join(root, "images", "logo.png"), "light")
+ writeFile(t, filepath.Join(root, "images", "dark", "logo.png"), "dark")
+ writeFile(t, filepath.Join(root, "about.md"), `---
+title: About
+---
+
+
+
+
+`)
+
+ specBytes, err := os.ReadFile(specPath)
+ require.NoError(t, err)
+ pp, err := CreatePrintingPressFromBytes(specBytes, &PrintingPressConfig{
+ BasePath: root,
+ SpecPath: specPath,
+ EnableContentPages: true,
+ })
+ require.NoError(t, err)
+
+ site, err := pp.PressModel()
+ require.NoError(t, err)
+ about := requireContentPage(t, site.ContentPages, "about")
+ require.Len(t, about.Assets, 2)
+ assert.Equal(t, "assets/docs/about/logo.png", about.Assets[0].Href)
+ assert.Equal(t, "assets/docs/about/logo-2.png", about.Assets[1].Href)
+ assert.Equal(t, 2, strings.Count(about.BodyHTML, `src="assets/docs/about/logo.png"`))
+ assert.Contains(t, about.BodyHTML, `src="assets/docs/about/logo-2.png"`)
+}
+
+func TestContentPages_InvalidFrontMatterKeepsOriginalBodyAndPartialsSkipCodeFences(t *testing.T) {
+ restoreLogger := silenceDefaultLogger()
+ defer restoreLogger()
+
+ root := t.TempDir()
+ specPath := filepath.Join(root, "openapi.yaml")
+ writeFile(t, specPath, minimalContentPageSpec())
+ writeFile(t, filepath.Join(root, "_partials", "intro.md"), "Reusable partial.\n")
+ writeFile(t, filepath.Join(root, "about.md"), "---\n"+
+ "## This is a markdown thematic break, not front matter.\n"+
+ "---\n\n"+
+ "Top section stays visible.\n\n"+
+ "```md\n"+
+ "{{
}}\n"+
+ "```\n\n"+
+ "{{}}\n")
+
+ specBytes, err := os.ReadFile(specPath)
+ require.NoError(t, err)
+ pp, err := CreatePrintingPressFromBytes(specBytes, &PrintingPressConfig{
+ BasePath: root,
+ SpecPath: specPath,
+ EnableContentPages: true,
+ })
+ require.NoError(t, err)
+
+ site, err := pp.PressModel()
+ require.NoError(t, err)
+ about := requireContentPage(t, site.ContentPages, "about")
+ assert.Contains(t, about.BodyHTML, "Top section stays visible.")
+ assert.Contains(t, about.BodyHTML, "This is a markdown thematic break")
+ assert.Equal(t, 1, strings.Count(about.BodyHTML, "Reusable partial."))
+ assert.Contains(t, about.BodyHTML, "{{<partial")
+}
+
+func TestContentPages_ShadowedDocsLinkRewritesToRootWinner(t *testing.T) {
+ restoreLogger := silenceDefaultLogger()
+ defer restoreLogger()
+
+ root := t.TempDir()
+ specPath := filepath.Join(root, "openapi.yaml")
+ writeFile(t, specPath, minimalContentPageSpec())
+ writeFile(t, filepath.Join(root, "about.md"), "See [Guide](docs/guide.md).\n")
+ writeFile(t, filepath.Join(root, "guide.md"), "Root guide wins.\n")
+ writeFile(t, filepath.Join(root, "docs", "guide.md"), "Ignored guide.\n")
+
+ specBytes, err := os.ReadFile(specPath)
+ require.NoError(t, err)
+ pp, err := CreatePrintingPressFromBytes(specBytes, &PrintingPressConfig{
+ BasePath: root,
+ SpecPath: specPath,
+ EnableContentPages: true,
+ })
+ require.NoError(t, err)
+
+ site, err := pp.PressModel()
+ require.NoError(t, err)
+ about := requireContentPage(t, site.ContentPages, "about")
+ assert.Contains(t, about.BodyHTML, `href="guide.html"`)
+ assert.NotContains(t, about.BodyHTML, `href="docs/guide.md"`)
+}
+
+func TestContentPages_ImageSymlinkEscapeIsNotCopied(t *testing.T) {
+ restoreLogger := silenceDefaultLogger()
+ defer restoreLogger()
+
+ root := t.TempDir()
+ outputDir := t.TempDir()
+ specPath := filepath.Join(root, "openapi.yaml")
+ writeFile(t, specPath, minimalContentPageSpec())
+ writeFile(t, filepath.Join(root, "about.md"), `---
+title: About
+---
+
+
+`)
+ outside := t.TempDir()
+ writeFile(t, filepath.Join(outside, "escape.svg"), ``)
+ require.NoError(t, os.MkdirAll(filepath.Join(root, "images"), 0o755))
+ if err := os.Symlink(filepath.Join(outside, "escape.svg"), filepath.Join(root, "images", "escape.svg")); err != nil {
+ t.Skipf("symlinks unavailable: %v", err)
+ }
+
+ specBytes, err := os.ReadFile(specPath)
+ require.NoError(t, err)
+ pp, err := CreatePrintingPressFromBytes(specBytes, &PrintingPressConfig{
+ BasePath: root,
+ SpecPath: specPath,
+ OutputDir: outputDir,
+ EnableContentPages: true,
+ })
+ require.NoError(t, err)
+
+ site, err := pp.PressModel()
+ require.NoError(t, err)
+ about := requireContentPage(t, site.ContentPages, "about")
+ assert.Contains(t, about.BodyHTML, `src="images/escape.svg"`)
+ assert.Empty(t, about.Assets)
+
+ _, err = pp.PrintHTML()
+ require.NoError(t, err)
+ assert.NoFileExists(t, filepath.Join(outputDir, "assets", "docs", "about", "escape.svg"))
+}
+
+func TestContentPageContext_RemoteSpecKeepsConfiguredLocalBasePath(t *testing.T) {
+ root := t.TempDir()
+ pp := &PrintingPress{engineConfig: &pressEngineConfig{
+ ContentDiscoveryEnabled: true,
+ ContentBasePath: root,
+ ContentSpecPath: "https://example.com/apis/openapi.yaml",
+ }}
+
+ ctx := pp.newContentPageContext()
+ require.NotNil(t, ctx)
+ assert.Equal(t, root, ctx.root)
+ assert.Equal(t, filepath.Join(root, "docs"), ctx.docsRoot)
+}
+
+func TestContentPages_GuidesIndexRouteOnlyReservesExactSlug(t *testing.T) {
+ assert.True(t, isReservedContentSlug("guides"))
+ assert.True(t, isReservedContentSlug("services/users/index"))
+ assert.True(t, isReservedContentSlug("assets/docs/about/logo.png"))
+ assert.False(t, isReservedContentSlug("guides/setup"))
+}
+
+func minimalContentPageSpec() string {
+ return `openapi: 3.1.0
+info:
+ title: Content Page Test API
+ version: 1.0.0
+paths:
+ /ping:
+ get:
+ operationId: getPing
+ summary: Ping
+ responses:
+ "200":
+ description: ok
+`
+}
+
+func requireContentPage(t *testing.T, pages []*ppmodel.ContentPage, slug string) *ppmodel.ContentPage {
+ t.Helper()
+ for _, page := range pages {
+ if page != nil && page.Slug == slug {
+ return page
+ }
+ }
+ require.Failf(t, "missing content page", "slug %q not found", slug)
+ return nil
+}
+
+func contentPageIndex(pages []*ppmodel.ContentPage, slug string) int {
+ for i, page := range pages {
+ if page != nil && page.Slug == slug {
+ return i
+ }
+ }
+ return -1
+}
+
+func writeFile(t *testing.T, path string, body string) {
+ t.Helper()
+ require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
+ require.NoError(t, os.WriteFile(path, []byte(body), 0o644))
+}
+
+func readFile(t *testing.T, path string) string {
+ t.Helper()
+ data, err := os.ReadFile(path)
+ require.NoError(t, err)
+ return string(data)
+}
+
+func silenceDefaultLogger() func() {
+ previous := slog.Default()
+ slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, nil)))
+ return func() {
+ slog.SetDefault(previous)
+ }
+}
diff --git a/printingpress/html_hydration.go b/printingpress/html_hydration.go
index 32e3753..af69e23 100644
--- a/printingpress/html_hydration.go
+++ b/printingpress/html_hydration.go
@@ -145,6 +145,7 @@ func buildSharedHydrationPayload(site *ppmodel.Site) *htmlHydrationPayload {
"data-nav": render.MustJSON(site.NavTags),
"data-models": render.MustJSON(site.NavModelGroups),
"data-webhooks": render.MustJSON(site.NavWebhooks),
+ "data-pages": render.MustJSON(contentPagesForNav(site.ContentPages)),
},
},
SchemaRegistry: registry,
diff --git a/printingpress/html_writer.go b/printingpress/html_writer.go
index a5c7f49..da097d8 100644
--- a/printingpress/html_writer.go
+++ b/printingpress/html_writer.go
@@ -107,6 +107,10 @@ func estimateHTMLPageJobs(site *ppmodel.Site) int {
if site.Root != nil {
total++
}
+ total += len(site.ContentPages)
+ if len(site.ContentPages) > 0 {
+ total++
+ }
total += len(site.Operations)
for typeSlug, pages := range site.Models {
if typeSlug == typeSlugPathItems {
@@ -254,6 +258,7 @@ func writeHTMLSiteDetailed(site *ppmodel.Site, outputDir, baseURL string, progre
ArchiveExportURL: site.ArchiveExportURL,
Footer: site.Footer,
SharedAssetBaseURL: site.SharedAssetBaseURL,
+ HasContentPages: hasContentPagesForNav(site.ContentPages),
}
var jobs []htmlWriteJob
@@ -283,6 +288,46 @@ func writeHTMLSiteDetailed(site *ppmodel.Site, outputDir, baseURL string, progre
progressTracker.advance("preparing overview page")
}
+ // Write convention-discovered content pages.
+ for _, page := range site.ContentPages {
+ if page == nil {
+ continue
+ }
+ assetPaths, err := writeContentPageAssets(resolvedOutputDir, page)
+ if err != nil {
+ return nil, err
+ }
+ staticPaths = append(staticPaths, assetPaths...)
+ p := *params
+ p.BaseURL = resolveBase(resolvedBaseURL, contentPageDepth(page.Href))
+ p.ExtraCSS = []string{pppaths.StaticAsset(pppaths.FilePrintingPressIndexCSS)}
+ content := render.ContentPageTempl(page, p.BaseURL)
+ jobs = append(jobs, htmlWriteJob{
+ path: filepath.Join(resolvedOutputDir, filepath.FromSlash(page.Href)),
+ pageTitle: fmt.Sprintf("%s - %s", page.Title, title),
+ activeSlug: "content/" + page.Slug,
+ params: p,
+ content: content,
+ })
+ progressTracker.advance("preparing content pages")
+ }
+
+ // Write generated guides index page.
+ if len(site.ContentPages) > 0 {
+ p := *params
+ p.BaseURL = resolvedBaseURL
+ p.ExtraCSS = []string{pppaths.StaticAsset(pppaths.FilePrintingPressIndexCSS)}
+ content := render.ContentIndexTempl(site.ContentPages, render.GuidesIndexBreadcrumb(), p.BaseURL)
+ jobs = append(jobs, htmlWriteJob{
+ path: filepath.Join(resolvedOutputDir, filepath.FromSlash(pppaths.GuidesIndexHTML())),
+ pageTitle: "Guides - " + title,
+ activeSlug: "guides",
+ params: p,
+ content: content,
+ })
+ progressTracker.advance("preparing guides index")
+ }
+
// Write operation pages (1 level deep: operations/)
for _, op := range site.Operations {
p := *params
@@ -652,6 +697,7 @@ type pageParams struct {
ArchiveExportURL string
Footer *ppmodel.FooterConfig
SharedAssetBaseURL string
+ HasContentPages bool
}
func writeTemplPage(path, pageTitle, activeSlug string, p *pageParams, content templ.Component) error {
@@ -688,6 +734,7 @@ func writeTemplPage(path, pageTitle, activeSlug string, p *pageParams, content t
ArchiveExportURL: p.ArchiveExportURL,
Footer: p.Footer,
SharedAssetBaseURL: p.SharedAssetBaseURL,
+ HasContentPages: p.HasContentPages,
}, content)
return layout.Render(context.Background(), f)
}
@@ -724,6 +771,27 @@ func resolveBase(baseURL string, depth int) string {
return strings.Repeat("../", depth)
}
+func writeContentPageAssets(outputDir string, page *ppmodel.ContentPage) ([]string, error) {
+ if page == nil {
+ return nil, nil
+ }
+ var written []string
+ for _, asset := range page.Assets {
+ if asset == nil || asset.Href == "" {
+ continue
+ }
+ assetPath := filepath.Join(outputDir, filepath.FromSlash(asset.Href))
+ if err := os.MkdirAll(filepath.Dir(assetPath), 0o755); err != nil {
+ return nil, fmt.Errorf("creating content asset directory: %w", err)
+ }
+ if err := os.WriteFile(assetPath, asset.Data, 0o644); err != nil {
+ return nil, fmt.Errorf("writing content asset %s: %w", asset.Href, err)
+ }
+ written = append(written, assetPath)
+ }
+ return written, nil
+}
+
func copyEmbeddedStatic(outputDir string) ([]string, error) {
return copyEmbeddedDir(pppaths.DirStatic, filepath.Join(outputDir, pppaths.DirStatic))
}
diff --git a/printingpress/internal/pppaths/constants.go b/printingpress/internal/pppaths/constants.go
index f626d81..4de13d0 100644
--- a/printingpress/internal/pppaths/constants.go
+++ b/printingpress/internal/pppaths/constants.go
@@ -24,6 +24,7 @@ const (
DirOperations = "operations"
DirModels = "models"
DirTags = "tags"
+ DirContent = "content"
DirServices = "services"
DirVersions = "versions"
DirSpecs = "specs"
@@ -35,18 +36,20 @@ const (
DirPages = "pages"
DirViz = "viz"
- FileIndexHTML = "index.html"
- FileDiagnosticsHTML = "diagnostics.html"
- FileIndexJSON = "index.json"
- FileBundleJSON = "bundle.json"
- FileManifestJSON = "manifest.json"
- FileNavJSON = "nav.json"
- FileLLMIndex = "llms.txt"
- FileLLMFull = "llms-full.txt"
- FileLLMOperations = "llms-operations.txt"
- FileLLMModels = "llms-models.txt"
- FileAgentsGuide = "AGENTS.md"
- FileStateSQLite = ".printingpress-state.db"
+ FileIndexHTML = "index.html"
+ FileGuidesHTML = "guides.html"
+ FileDiagnosticsHTML = "diagnostics.html"
+ FileIndexJSON = "index.json"
+ FileBundleJSON = "bundle.json"
+ FileManifestJSON = "manifest.json"
+ FileNavJSON = "nav.json"
+ FileLLMIndex = "llms.txt"
+ FileLLMFull = "llms-full.txt"
+ FileLLMOperations = "llms-operations.txt"
+ FileLLMModels = "llms-models.txt"
+ FileAgentsGuide = "AGENTS.md"
+ FileStateSQLite = ".printingpress-state.db"
+ FileCatalogContentStateJSON = ".printingpress-catalog-content.json"
FilePB33FThemeCSS = "pb33f-theme.css"
FileCowboyComponentsCSS = "cowboy-components.css"
@@ -120,6 +123,10 @@ func ModelsIndexHTML() string {
return path.Join(DirModels, FileIndexHTML)
}
+func GuidesIndexHTML() string {
+ return FileGuidesHTML
+}
+
func ModelTypeIndexHTML(typeSlug string) string {
return path.Join(DirModels, typeSlug, FileIndexHTML)
}
@@ -140,6 +147,18 @@ func TagHTML(slug string) string {
return path.Join(DirTags, slug+ExtHTML)
}
+func ContentPageHTML(slugPath string) string {
+ return path.Clean(slugPath) + ExtHTML
+}
+
+func ContentPageDataBase(slugPath string) string {
+ return path.Join(PageDataDir(), DirContent, path.Clean(slugPath))
+}
+
+func ContentAsset(pageSlug, assetName string) string {
+ return path.Join(DirAssets, "docs", path.Clean(pageSlug), assetName)
+}
+
func OperationPageDataBase(slug string) string {
return path.Join(PageDataDir(), DirOperations, slug)
}
diff --git a/printingpress/json_artifacts.go b/printingpress/json_artifacts.go
index 7e0e9e1..11577dc 100644
--- a/printingpress/json_artifacts.go
+++ b/printingpress/json_artifacts.go
@@ -22,6 +22,7 @@ type JSONBundle struct {
Root *JSONRootPage `json:"root,omitempty"`
Source *ppmodel.SourceRef `json:"source,omitempty"`
Nav []*ppmodel.NavTag `json:"nav,omitempty"`
+ ContentPages []*ppmodel.ContentPage `json:"contentPages,omitempty"`
ModelGroups []*ppmodel.NavModelGroup `json:"modelGroups,omitempty"`
SchemaRegistry map[string]*ppmodel.SchemaRegistryEntry `json:"schemaRegistry,omitempty"`
Operations []JSONArtifactEntry `json:"operations,omitempty"`
@@ -123,6 +124,7 @@ func buildJSONBundle(site *ppmodel.Site) *JSONBundle {
Root: buildJSONRootPage(site.Root),
Source: site.Source,
Nav: site.NavTags,
+ ContentPages: site.ContentPages,
ModelGroups: site.NavModelGroups,
SchemaRegistry: site.SchemaRegistry,
Operations: buildOperationArtifactEntries(site.Operations, "operation"),
diff --git a/printingpress/markdown.go b/printingpress/markdown.go
index 441a886..0344c5a 100644
--- a/printingpress/markdown.go
+++ b/printingpress/markdown.go
@@ -25,11 +25,12 @@ func init() {
mdRendererPlain = newMarkdownRenderer(false)
}
-func newMarkdownRenderer(includeMermaid bool) goldmark.Markdown {
+func newMarkdownRenderer(includeMermaid bool, extraExtensions ...goldmark.Extender) goldmark.Markdown {
extensions := []goldmark.Extender{extension.GFM}
if includeMermaid {
extensions = append(extensions, &mermaidExtension{})
}
+ extensions = append(extensions, extraExtensions...)
extensions = append(extensions, highlighting.NewHighlighting(
highlighting.WithStyle("dracula"),
highlighting.WithFormatOptions(
@@ -67,6 +68,18 @@ func renderMarkdown(md string, noMermaid bool) string {
return buf.String()
}
+func renderMarkdownWithExtensions(md string, noMermaid bool, extraExtensions ...goldmark.Extender) string {
+ if md == "" {
+ return ""
+ }
+ renderer := newMarkdownRenderer(!noMermaid, extraExtensions...)
+ var buf bytes.Buffer
+ if err := renderer.Convert([]byte(md), &buf); err != nil {
+ return md
+ }
+ return buf.String()
+}
+
func (pp *PrintingPress) renderMarkdown(md string) string {
return renderMarkdown(md, pp.engineConfig.NoMermaid)
}
diff --git a/printingpress/markdown_content_rewrite.go b/printingpress/markdown_content_rewrite.go
new file mode 100644
index 0000000..f3e1569
--- /dev/null
+++ b/printingpress/markdown_content_rewrite.go
@@ -0,0 +1,68 @@
+// Copyright 2024-2026 Princess Beef Heavy Industries, LLC / Dave Shanley
+// https://pb33f.io
+// SPDX-License-Identifier: Apache-2.0
+
+package printingpress
+
+import (
+ "strings"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+type contentRewriteExtension struct {
+ rewriteLink func(string) (string, bool)
+ rewriteImage func(string) (string, bool)
+}
+
+func (e *contentRewriteExtension) Extend(m goldmark.Markdown) {
+ m.Parser().AddOptions(parser.WithASTTransformers(
+ util.Prioritized(&contentRewriteTransformer{
+ rewriteLink: e.rewriteLink,
+ rewriteImage: e.rewriteImage,
+ }, 90),
+ ))
+}
+
+type contentRewriteTransformer struct {
+ rewriteLink func(string) (string, bool)
+ rewriteImage func(string) (string, bool)
+}
+
+func (t *contentRewriteTransformer) Transform(doc *ast.Document, reader text.Reader, pc parser.Context) {
+ _ = ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ switch node := n.(type) {
+ case *ast.Image:
+ if t.rewriteImage != nil {
+ if replacement, ok := t.rewriteImage(string(node.Destination)); ok {
+ node.Destination = []byte(replacement)
+ }
+ }
+ return ast.WalkContinue, nil
+ case *ast.Link:
+ if t.rewriteLink != nil {
+ if replacement, ok := t.rewriteLink(string(node.Destination)); ok {
+ node.Destination = []byte(replacement)
+ }
+ }
+ return ast.WalkContinue, nil
+ default:
+ return ast.WalkContinue, nil
+ }
+ })
+}
+
+func splitMarkdownHref(raw string) (target string, fragment string) {
+ target = strings.TrimSpace(raw)
+ if before, after, ok := strings.Cut(target, "#"); ok {
+ return before, "#" + after
+ }
+ return target, ""
+}
diff --git a/printingpress/model/catalog.go b/printingpress/model/catalog.go
index 4b2e96d..ac620e1 100644
--- a/printingpress/model/catalog.go
+++ b/printingpress/model/catalog.go
@@ -23,14 +23,15 @@ type SiteVersionLink struct {
// CatalogSite is the aggregate multi-spec documentation catalog.
type CatalogSite struct {
- Title string `json:"title,omitempty"`
- Description string `json:"description,omitempty"`
- ScanRoot string `json:"scanRoot,omitempty"`
- OutputDir string `json:"-"`
- BaseURL string `json:"-"`
- AssetMode string `json:"-"`
- Services []*CatalogService `json:"services,omitempty"`
- Warnings []*BuildWarning `json:"warnings,omitempty"`
+ Title string `json:"title,omitempty"`
+ Description string `json:"description,omitempty"`
+ ScanRoot string `json:"scanRoot,omitempty"`
+ OutputDir string `json:"-"`
+ BaseURL string `json:"-"`
+ AssetMode string `json:"-"`
+ Services []*CatalogService `json:"services,omitempty"`
+ ContentPages []*ContentPage `json:"contentPages,omitempty"`
+ Warnings []*BuildWarning `json:"warnings,omitempty"`
}
// CatalogService represents one grouped service in the aggregate catalog.
diff --git a/printingpress/model/models.go b/printingpress/model/models.go
index 2e8a422..5164adf 100644
--- a/printingpress/model/models.go
+++ b/printingpress/model/models.go
@@ -19,6 +19,7 @@ import (
type Site struct {
Root *RootPage
Operations []*OperationPage
+ ContentPages []*ContentPage
Models map[string][]*ModelPage // keyed by component type slug (e.g. "schemas")
Webhooks []*OperationPage
Diagnostics *DiagnosticsPage
@@ -47,6 +48,31 @@ type Site struct {
HeaderContext *SiteHeaderContext `json:"headerContext,omitempty"`
}
+// ContentPage is a convention-discovered Markdown page rendered alongside the
+// generated API reference.
+type ContentPage struct {
+ Title string `json:"title"`
+ Label string `json:"label,omitempty"`
+ Slug string `json:"slug"`
+ Href string `json:"href"`
+ Description string `json:"description,omitempty"`
+ SourcePath string `json:"-"`
+ SourceDir string `json:"-"`
+ SourceAlias []string `json:"-"`
+ Body string `json:"-"`
+ BodyHTML string `json:"bodyHtml,omitempty"`
+ Order int `json:"order,omitempty"`
+ Hidden bool `json:"hidden,omitempty"`
+ Assets []*ContentPageAsset `json:"-"`
+}
+
+// ContentPageAsset is a local or remote image copied into the generated docs.
+type ContentPageAsset struct {
+ Href string `json:"href"`
+ SourcePath string `json:"-"`
+ Data []byte `json:"-"`
+}
+
// LLMOutputConfig controls LLM-oriented aggregate text artifacts for a site.
type LLMOutputConfig struct {
AggregateSpecSizeThresholdBytes int64
diff --git a/printingpress/press.go b/printingpress/press.go
index c85e267..6b66402 100644
--- a/printingpress/press.go
+++ b/printingpress/press.go
@@ -26,39 +26,42 @@ import (
)
type pressEngineConfig struct {
- DrDoc *doctormodel.DrDocument
- Origins bundler.ComponentOriginMap
- OutputDir string
- BaseURL string
- AssetMode string
- Embedded bool
- SharedAssetBaseURL string
- Title string
- Logger *slog.Logger
- SpecFormat string // "yaml" or "json" — caller should set based on input format
- SpecRoot string // root directory of the spec; absolute paths are made relative to this
- SpecPath string // absolute local path to the root spec file when known
- SpecLocation string // display path of the root spec file relative to SpecRoot when possible
- SpecURL string // optional published source URL for the root spec file
- SourceSizeBytes int64 // size of the root source bytes when known
- NoMermaid bool // skip mermaid class diagram generation on model pages
- NoExplorer bool // skip dependency explorer on model pages
- BuildWarnings []*BuildWarning
- BuildErrors []error // errors from libopenapi model building (surfaced as warnings on index page)
- DeveloperMode bool
- DocsExpiresAt string
- ArchiveExportURL string
- LintResults []*v3.RuleFunctionResult
- OrphanResults []*v3.RuleFunctionResult
- Footer *FooterConfig
- MaxPatternRepeatBudget int
- MaxGeneratedStringBytes int
- MaxGeneratedMockBytes int
- MaxMockDepth int
- MaxMockNodes int
- MaxMockProperties int
- MaxMockRefExpansions int
- MaxMockBytes int
+ DrDoc *doctormodel.DrDocument
+ Origins bundler.ComponentOriginMap
+ OutputDir string
+ BaseURL string
+ AssetMode string
+ Embedded bool
+ SharedAssetBaseURL string
+ Title string
+ Logger *slog.Logger
+ SpecFormat string // "yaml" or "json" — caller should set based on input format
+ SpecRoot string // root directory of the spec; absolute paths are made relative to this
+ SpecPath string // absolute local path to the root spec file when known
+ SpecLocation string // display path of the root spec file relative to SpecRoot when possible
+ SpecURL string // optional published source URL for the root spec file
+ ContentDiscoveryEnabled bool
+ ContentBasePath string
+ ContentSpecPath string
+ SourceSizeBytes int64 // size of the root source bytes when known
+ NoMermaid bool // skip mermaid class diagram generation on model pages
+ NoExplorer bool // skip dependency explorer on model pages
+ BuildWarnings []*BuildWarning
+ BuildErrors []error // errors from libopenapi model building (surfaced as warnings on index page)
+ DeveloperMode bool
+ DocsExpiresAt string
+ ArchiveExportURL string
+ LintResults []*v3.RuleFunctionResult
+ OrphanResults []*v3.RuleFunctionResult
+ Footer *FooterConfig
+ MaxPatternRepeatBudget int
+ MaxGeneratedStringBytes int
+ MaxGeneratedMockBytes int
+ MaxMockDepth int
+ MaxMockNodes int
+ MaxMockProperties int
+ MaxMockRefExpansions int
+ MaxMockBytes int
LLMAggregateSpecSizeThresholdBytes int64
LLMMaxAggregateFileBytes int64
LLMGenerateMonoliths string
@@ -386,6 +389,7 @@ func (pp *PrintingPress) pressSite() (*Site, error) {
}
pp.site.NoMermaid = pp.engineConfig.NoMermaid
pp.site.Lite = pp.engineConfig.NoMermaid && pp.engineConfig.NoExplorer
+ pp.collectContentPages()
return pp.site, nil
}
diff --git a/printingpress/render/bootstrap_scripts/shared_nav_cache.js b/printingpress/render/bootstrap_scripts/shared_nav_cache.js
index 50ca5a6..869d1d4 100644
--- a/printingpress/render/bootstrap_scripts/shared_nav_cache.js
+++ b/printingpress/render/bootstrap_scripts/shared_nav_cache.js
@@ -127,6 +127,10 @@
return docHref('models/' + typeSlug + '/' + slug + '.html');
}
+ function contentPageActiveSlug(slug) {
+ return 'content/' + String(slug || '');
+ }
+
function parseJSONAttr(raw) {
if (Array.isArray(raw)) {
return raw;
@@ -314,14 +318,34 @@
return html;
}
+ function renderContentPage(page, activeSlug) {
+ if (!page) {
+ return '';
+ }
+ const classes = ['nav-page-link'];
+ if (contentPageActiveSlug(page.slug) === activeSlug) {
+ classes.push('active');
+ }
+ return (
+ "" +
+ escapeHtml(page.label || page.title || page.slug || 'Untitled') +
+ ''
+ );
+ }
+
function renderNavPreview(navAttrs, activeSlug) {
if (!navAttrs) {
return '';
}
const tags = parseJSONAttr(navAttrs['data-nav']);
+ const pages = parseJSONAttr(navAttrs['data-pages']);
const modelGroups = parseJSONAttr(navAttrs['data-models']);
const webhooks = parseJSONAttr(navAttrs['data-webhooks']);
- if (!tags.length && !modelGroups.length && !webhooks.length) {
+ if (!tags.length && !pages.length && !modelGroups.length && !webhooks.length) {
return '';
}
let html =
@@ -340,6 +364,13 @@
escapeHtml(docHref('diagnostics.html')) +
"'>DIAGNOSTICS";
}
+ if (pages.length) {
+ html += "Guides
";
+ for (let i = 0; i < pages.length; i++) {
+ html += renderContentPage(pages[i], activeSlug);
+ }
+ html += '
';
+ }
if (tags.length) {
html += "Operations
";
for (let i = 0; i < tags.length; i++) {
@@ -418,6 +449,7 @@
log('nav-preview:rendered', {
source: 'shared-cache-bootstrap',
tags: parseJSONAttr(navAttrs['data-nav']).length,
+ pages: parseJSONAttr(navAttrs['data-pages']).length,
modelGroups: parseJSONAttr(navAttrs['data-models']).length,
webhooks: parseJSONAttr(navAttrs['data-webhooks']).length,
});
@@ -430,6 +462,7 @@
log('nav-fallback:retained', {
source: 'shared-cache-bootstrap',
tags: parseJSONAttr(navAttrs['data-nav']).length,
+ pages: parseJSONAttr(navAttrs['data-pages']).length,
modelGroups: parseJSONAttr(navAttrs['data-models']).length,
webhooks: parseJSONAttr(navAttrs['data-webhooks']).length,
});
diff --git a/printingpress/render/content_index.go b/printingpress/render/content_index.go
new file mode 100644
index 0000000..bd5f626
--- /dev/null
+++ b/printingpress/render/content_index.go
@@ -0,0 +1,73 @@
+// Copyright 2024-2026 Princess Beef Heavy Industries, LLC / Dave Shanley
+// https://pb33f.io
+// SPDX-License-Identifier: Apache-2.0
+
+package render
+
+import (
+ "context"
+ "io"
+
+ "github.com/a-h/templ"
+ ppmodel "github.com/pb33f/doctor/printingpress/model"
+)
+
+// ContentIndexTempl renders the generated index for convention-discovered
+// Markdown pages.
+func ContentIndexTempl(pages []*ppmodel.ContentPage, breadcrumb []BreadcrumbItem, baseURL string) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
+ if _, err := io.WriteString(w, `
`); err != nil {
+ return err
+ }
+ if err := Breadcrumb(baseURL, breadcrumb).Render(ctx, w); err != nil {
+ return err
+ }
+ if _, err := io.WriteString(w, `
Guides
`); err != nil {
+ return err
+ }
+ wrote := false
+ for _, page := range pages {
+ if page == nil || page.Hidden {
+ continue
+ }
+ wrote = true
+ if _, err := io.WriteString(w, `
`); err != nil {
+ return err
+ }
+ if _, err := io.WriteString(w, ``+templ.EscapeString(contentPageIndexLabel(page))+`
`); err != nil {
+ return err
+ }
+ if page.Description != "" {
+ if _, err := io.WriteString(w, ``+templ.EscapeString(truncateDesc(page.Description, 140))+`
`); err != nil {
+ return err
+ }
+ }
+ if _, err := io.WriteString(w, ``); err != nil {
+ return err
+ }
+ }
+ if !wrote {
+ if _, err := io.WriteString(w, `
No guides are linked.
`); err != nil {
+ return err
+ }
+ }
+ _, err := io.WriteString(w, `
`)
+ return err
+ })
+}
+
+func contentPageIndexLabel(page *ppmodel.ContentPage) string {
+ if page == nil {
+ return "Untitled"
+ }
+ if page.Label != "" {
+ return page.Label
+ }
+ if page.Title != "" {
+ return page.Title
+ }
+ if page.Slug != "" {
+ return slugToTitle(pathBaseSlug(page.Slug))
+ }
+ return "Untitled"
+}
diff --git a/printingpress/render/content_page.go b/printingpress/render/content_page.go
new file mode 100644
index 0000000..ba6178d
--- /dev/null
+++ b/printingpress/render/content_page.go
@@ -0,0 +1,51 @@
+// Copyright 2024-2026 Princess Beef Heavy Industries, LLC / Dave Shanley
+// https://pb33f.io
+// SPDX-License-Identifier: Apache-2.0
+
+package render
+
+import (
+ "context"
+ "io"
+
+ "github.com/a-h/templ"
+ ppmodel "github.com/pb33f/doctor/printingpress/model"
+)
+
+// ContentPageTempl renders a convention-discovered Markdown page inside the
+// shared Printing Press page shell.
+func ContentPageTempl(page *ppmodel.ContentPage, baseURL string) templ.Component {
+ return ContentPageTemplWithBreadcrumb(page, baseURL, contentPageBreadcrumb(page))
+}
+
+// ContentPageTemplWithBreadcrumb renders a convention-discovered Markdown page
+// with caller-provided breadcrumb items.
+func ContentPageTemplWithBreadcrumb(page *ppmodel.ContentPage, baseURL string, breadcrumb []BreadcrumbItem) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
+ if page == nil {
+ return nil
+ }
+ if _, err := io.WriteString(w, `
`); err != nil {
+ return err
+ }
+ if err := Breadcrumb(baseURL, breadcrumb).Render(ctx, w); err != nil {
+ return err
+ }
+ if _, err := io.WriteString(w, ``); err != nil {
+ return err
+ }
+ if err := templ.Raw(page.BodyHTML).Render(ctx, w); err != nil {
+ return err
+ }
+ _, err := io.WriteString(w, `
`)
+ return err
+ })
+}
diff --git a/printingpress/render/helpers.go b/printingpress/render/helpers.go
index 6564a6f..7ec3e3e 100644
--- a/printingpress/render/helpers.go
+++ b/printingpress/render/helpers.go
@@ -51,6 +51,25 @@ func operationBreadcrumb(page *ppmodel.OperationPage) []BreadcrumbItem {
return items
}
+func contentPageBreadcrumb(page *ppmodel.ContentPage) []BreadcrumbItem {
+ label := "PAGE"
+ if page != nil {
+ switch {
+ case strings.TrimSpace(page.Label) != "":
+ label = strings.ToUpper(strings.TrimSpace(page.Label))
+ case strings.TrimSpace(page.Title) != "":
+ label = strings.ToUpper(strings.TrimSpace(page.Title))
+ case strings.TrimSpace(page.Slug) != "":
+ label = strings.ToUpper(slugToTitle(pathBaseSlug(page.Slug)))
+ }
+ }
+ return []BreadcrumbItem{
+ {Label: "HOME", Href: pppaths.FileIndexHTML},
+ {Label: "GUIDES", Href: pppaths.GuidesIndexHTML()},
+ {Label: label},
+ }
+}
+
// AssetHref resolves a relative asset reference against the configured hosted docs root.
// When no hosted docs root is configured, the original relative asset path is preserved.
func AssetHref(assetBaseURL, href string) string {
@@ -130,6 +149,14 @@ func ModelsIndexBreadcrumb() []BreadcrumbItem {
}
}
+// GuidesIndexBreadcrumb builds the breadcrumb for the generated custom content index.
+func GuidesIndexBreadcrumb() []BreadcrumbItem {
+ return []BreadcrumbItem{
+ {Label: "HOME", Href: pppaths.FileIndexHTML},
+ {Label: "GUIDES"},
+ }
+}
+
// ModelTypeIndexBreadcrumb builds the breadcrumb for a model type index page.
func ModelTypeIndexBreadcrumb(typeName string) []BreadcrumbItem {
return []BreadcrumbItem{
@@ -187,6 +214,15 @@ func slugToTitle(slug string) string {
return strings.Join(words, " ")
}
+func pathBaseSlug(slugPath string) string {
+ slugPath = strings.Trim(strings.TrimSpace(slugPath), "/")
+ if slugPath == "" {
+ return ""
+ }
+ parts := strings.Split(slugPath, "/")
+ return parts[len(parts)-1]
+}
+
func operationNavSections(page *ppmodel.OperationPage) string {
type navSection struct {
Label string `json:"label"`
diff --git a/printingpress/render/layout_page.go b/printingpress/render/layout_page.go
index 19c8eaf..07ff0fc 100644
--- a/printingpress/render/layout_page.go
+++ b/printingpress/render/layout_page.go
@@ -39,6 +39,7 @@ type LayoutPageParams struct {
ArchiveExportURL string
Footer *ppmodel.FooterConfig
SharedAssetBaseURL string
+ HasContentPages bool
}
// LayoutPage renders the shared printing press shell with optional aggregate header context.
@@ -160,10 +161,15 @@ func LayoutPage(params LayoutPageParams, content templ.Component) templ.Componen
if err := writeOptionalAttr(w, "data-archive-export-url", params.ArchiveExportURL); err != nil {
return err
}
+ if params.HasContentPages {
+ if err := writeOptionalAttr(w, "data-has-content-pages", "true"); err != nil {
+ return err
+ }
+ }
if _, err := io.WriteString(w, `>`); err != nil {
return err
}
- if _, err := io.WriteString(w, navFallbackHTML(params.DocsExpiresAt, params.DeveloperMode, params.ArchiveExportURL)); err != nil {
+ if _, err := io.WriteString(w, navFallbackHTML(params.DocsExpiresAt, params.DeveloperMode, params.ArchiveExportURL, params.HasContentPages)); err != nil {
return err
}
if err := BootstrapSharedNavCacheScript(params.AssetBaseURL, params.SharedDataBase, params.SharedDataHash).Render(ctx, w); err != nil {
@@ -201,7 +207,7 @@ const (
const embeddedReadyScript = ``
-func navFallbackHTML(docsExpiresAt string, developerMode bool, archiveExportURL string) string {
+func navFallbackHTML(docsExpiresAt string, developerMode bool, archiveExportURL string, hasContentPages bool) string {
expiry := ""
if strings.TrimSpace(docsExpiresAt) != "" {
expiry = `
docs expire
`
@@ -214,7 +220,11 @@ func navFallbackHTML(docsExpiresAt string, developerMode bool, archiveExportURL
if strings.TrimSpace(archiveExportURL) != "" {
archiveControls = navFallbackArchiveControlsHTML()
}
- return `
` + archiveControls + expiry + `
API OVERVIEW
` + diagnostics + `
`
+ guides := ""
+ if hasContentPages {
+ guides = `
`
+ }
+ return `
` + archiveControls + expiry + `
API OVERVIEW
` + diagnostics + guides + `
`
}
func navFallbackArchiveControlsHTML() string {
diff --git a/printingpress/render/layout_page_test.go b/printingpress/render/layout_page_test.go
index 6c43cf8..4b7d6f0 100644
--- a/printingpress/render/layout_page_test.go
+++ b/printingpress/render/layout_page_test.go
@@ -40,6 +40,24 @@ func TestLayoutPageDefaultFallbackOmitsDiagnostics(t *testing.T) {
if strings.Contains(html, `
`) {
t.Fatalf("did not expect archive controls in default nav fallback")
}
+ if strings.Contains(html, `
Guides
`) {
+ t.Fatalf("did not expect guides placeholder without content pages")
+ }
+}
+
+func TestLayoutPageContentFallbackIncludesGuides(t *testing.T) {
+ html := renderLayoutPageForTest(t, LayoutPageParams{
+ PageTitle: "Train Travel",
+ SiteTitle: "Train Travel",
+ HasContentPages: true,
+ })
+
+ if !strings.Contains(html, `data-has-content-pages="true"`) {
+ t.Fatalf("expected content page hint on nav element")
+ }
+ if !strings.Contains(html, `
Guides
`) {
+ t.Fatalf("expected guides placeholder in content page fallback")
+ }
}
func TestLayoutPageSharedAssetBaseURLAttribute(t *testing.T) {
diff --git a/printingpress/render/templ_layout.templ b/printingpress/render/templ_layout.templ
index fff591b..8f305b3 100644
--- a/printingpress/render/templ_layout.templ
+++ b/printingpress/render/templ_layout.templ
@@ -5,7 +5,7 @@ import "context"
import "io"
import "strings"
-templ Layout(pageTitle string, siteTitle string, baseURL string, assetBaseURL string, sharedAssetBaseURL string, activeSlug string, specFormat string, assetMode string, sharedDataBase string, pageDataBase string, vizGraphDataBase string, vizDiagramDataBase string, extraCSS []string, lite bool, developerMode bool, content templ.Component) {
+templ Layout(pageTitle string, siteTitle string, baseURL string, assetBaseURL string, sharedAssetBaseURL string, activeSlug string, specFormat string, assetMode string, sharedDataBase string, pageDataBase string, vizGraphDataBase string, vizDiagramDataBase string, extraCSS []string, lite bool, developerMode bool, hasContentPages bool, content templ.Component) {
@Head(pageTitle, baseURL, assetBaseURL, sharedAssetBaseURL, extraCSS, lite)
@@ -32,12 +32,30 @@ templ Layout(pageTitle string, siteTitle string, baseURL string, assetBaseURL st
if developerMode {
data-pp-developer-mode="true"
}>
-
-
-
-
-
API OVERVIEW
-
+
+
+
+
+
API OVERVIEW
+ if hasContentPages {
+
+ }
+
Operations
diff --git a/printingpress/render/templ_layout_templ.go b/printingpress/render/templ_layout_templ.go
index eac2137..6e3ea2e 100644
--- a/printingpress/render/templ_layout_templ.go
+++ b/printingpress/render/templ_layout_templ.go
@@ -13,7 +13,7 @@ import "context"
import "io"
import "strings"
-func Layout(pageTitle string, siteTitle string, baseURL string, assetBaseURL string, sharedAssetBaseURL string, activeSlug string, specFormat string, assetMode string, sharedDataBase string, pageDataBase string, vizGraphDataBase string, vizDiagramDataBase string, extraCSS []string, lite bool, developerMode bool, content templ.Component) templ.Component {
+func Layout(pageTitle string, siteTitle string, baseURL string, assetBaseURL string, sharedAssetBaseURL string, activeSlug string, specFormat string, assetMode string, sharedDataBase string, pageDataBase string, vizGraphDataBase string, vizDiagramDataBase string, extraCSS []string, lite bool, developerMode bool, hasContentPages bool, content templ.Component) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -186,7 +186,7 @@ func Layout(pageTitle string, siteTitle string, baseURL string, assetBaseURL str
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(siteTitle)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_layout.templ`, Line: 35, Col: 36}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_layout.templ`, Line: 35, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -199,7 +199,7 @@ func Layout(pageTitle string, siteTitle string, baseURL string, assetBaseURL str
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(siteTitle)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_layout.templ`, Line: 36, Col: 158}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_layout.templ`, Line: 36, Col: 159}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -212,13 +212,33 @@ func Layout(pageTitle string, siteTitle string, baseURL string, assetBaseURL str
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(activeSlug)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_layout.templ`, Line: 37, Col: 59}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_layout.templ`, Line: 40, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if hasContentPages {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " data-has-content-pages=\"true\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, ">API OVERVIEW
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if hasContentPages {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -226,7 +246,7 @@ func Layout(pageTitle string, siteTitle string, baseURL string, assetBaseURL str
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -234,7 +254,7 @@ func Layout(pageTitle string, siteTitle string, baseURL string, assetBaseURL str
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "