diff --git a/AGENTS.md b/AGENTS.md
index 22bbf6b..5cf10b7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -15,6 +15,11 @@ between first-party entities (cms, orchestrator, ninja CLI).
surface here — `plugin/`, `plugin/wasmguest/**`, and the guest-facing type
packages — is **one language binding** of the SDK. A non-Go plugin implements
the ABI hooks directly and never sees these Go structs.
+- **The SDK ships no page chrome.** A template renders only its
+ `TemplateDocument`; the **host** composes the document envelope
+ (doctype / `` / `
` / `` + bn chrome) around it. A guest
+ expresses its envelope needs (title, meta, asset hooks) through
+ `templates.PageDocument` — never by emitting head/body markup itself.
## Critical Rules
@@ -25,15 +30,6 @@ between first-party entities (cms, orchestrator, ninja CLI).
API and update consumers.
- Regenerate Go bindings after editing any `.proto`: `make proto`.
-## `templates/bn/` is a synced copy — do not author here
-
-The bn page chrome (head / toolbar / engagement / asset_hooks) is **authored in
-`cms/backend/templates/bn`** and mechanically copied here via cms `make
-sync-templates`, solely so guest-side plugin templates can compile it into their
-wasm. check-safety **check 31** fails cms commits while the copies drift. Never
-edit these files here directly — change them in cms and sync. (This chrome is
-transitional: it is deleted once host-side head/body injection lands — Phase 3.)
-
## Design
Program spec (in the cms repo):
diff --git a/templates/bn/asset_hooks.go b/templates/bn/asset_hooks.go
deleted file mode 100644
index 20525df..0000000
--- a/templates/bn/asset_hooks.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package bn
-
-// VersionedAssetURL resolves an asset URL to a cache-versioned form. The
-// default is the identity function; the CMS host wires it to
-// internal/assets.VersionedURL at startup so plugin stylesheet links get
-// content-hash ?v= params. Guest-side (wasm) renders keep the identity
-// default — asset hashing is a host concern.
-//
-// This file is part of the cms→core template sync set (make sync-templates);
-// it must stay SDK-clean (no cms imports).
-var VersionedAssetURL = func(url string) string { return url }
diff --git a/templates/bn/engagement.templ b/templates/bn/engagement.templ
deleted file mode 100644
index b1a7d82..0000000
--- a/templates/bn/engagement.templ
+++ /dev/null
@@ -1,514 +0,0 @@
-package bn
-
-import (
- "encoding/json"
-)
-
-// EngagementConfig contains configuration for the engagement tracker
-type EngagementConfig struct {
- PagePath string `json:"pagePath"`
- PageID string `json:"pageId"`
- PostWordCount int `json:"postWordCount"`
- SessionID string `json:"sessionId"`
- VisitorHash string `json:"visitorHash"`
- IsPost bool `json:"isPost"`
-}
-
-// engagementConfigJSON converts the config to JSON for embedding in the script
-func engagementConfigJSON(config EngagementConfig) string {
- data, err := json.Marshal(config)
- if err != nil {
- return "{}"
- }
- return string(data)
-}
-
-// EngagementScript renders the blog post engagement tracking script.
-// Only renders if IsPost is true to avoid bloating regular pages.
-templ EngagementScript(config EngagementConfig) {
- if config.IsPost && config.PageID != "" {
-
- }
-}
diff --git a/templates/bn/engagement_templ.go b/templates/bn/engagement_templ.go
deleted file mode 100644
index 6a9d669..0000000
--- a/templates/bn/engagement_templ.go
+++ /dev/null
@@ -1,80 +0,0 @@
-// Code generated by templ - DO NOT EDIT.
-
-// templ: version: v0.3.1020
-package bn
-
-//lint:file-ignore SA4006 This context is only used if a nested component is present.
-
-import "github.com/a-h/templ"
-import templruntime "github.com/a-h/templ/runtime"
-
-import (
- "encoding/json"
-)
-
-// EngagementConfig contains configuration for the engagement tracker
-type EngagementConfig struct {
- PagePath string `json:"pagePath"`
- PageID string `json:"pageId"`
- PostWordCount int `json:"postWordCount"`
- SessionID string `json:"sessionId"`
- VisitorHash string `json:"visitorHash"`
- IsPost bool `json:"isPost"`
-}
-
-// engagementConfigJSON converts the config to JSON for embedding in the script
-func engagementConfigJSON(config EngagementConfig) string {
- data, err := json.Marshal(config)
- if err != nil {
- return "{}"
- }
- return string(data)
-}
-
-// EngagementScript renders the blog post engagement tracking script.
-// Only renders if IsPost is true to avoid bloating regular pages.
-func EngagementScript(config EngagementConfig) 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 {
- return templ_7745c5c3_CtxErr
- }
- templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
- if !templ_7745c5c3_IsBuffer {
- defer func() {
- templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err == nil {
- templ_7745c5c3_Err = templ_7745c5c3_BufErr
- }
- }()
- }
- ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var1 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var1 == nil {
- templ_7745c5c3_Var1 = templ.NopComponent
- }
- ctx = templ.ClearChildren(ctx)
- if config.IsPost && config.PageID != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- return nil
- })
-}
-
-var _ = templruntime.GeneratedTemplate
diff --git a/templates/bn/head.templ b/templates/bn/head.templ
deleted file mode 100644
index cfd2e26..0000000
--- a/templates/bn/head.templ
+++ /dev/null
@@ -1,961 +0,0 @@
-package bn
-
-import (
- "strings"
- "time"
-
- "github.com/google/uuid"
-)
-
-// resolveMediaURL converts media: prefixed URLs to proper /media/ paths
-// e.g., "media:abc/image.webp" → "/media/abc/image.webp"
-func resolveMediaURL(url string) string {
- if strings.HasPrefix(url, "media:") {
- return "/media/" + strings.TrimPrefix(url, "media:")
- }
- return url
-}
-
-// canonicalURL resolves the canonical link. An admin-set value wins (made absolute
-// against the page's own origin when it is root-relative); otherwise the page's own
-// absolute URL is used as a self-referencing canonical. Returns "" only when neither
-// is available.
-func canonicalURL(canonical, pageURL string) string {
- if canonical == "" {
- return pageURL
- }
- if strings.HasPrefix(canonical, "http://") || strings.HasPrefix(canonical, "https://") {
- return canonical
- }
- if strings.HasPrefix(canonical, "/") && pageURL != "" {
- if i := strings.Index(pageURL, "://"); i >= 0 {
- if j := strings.IndexByte(pageURL[i+3:], '/'); j >= 0 {
- return pageURL[:i+3+j] + canonical
- }
- return pageURL + canonical
- }
- }
- return canonical
-}
-
-// BrandingData contains generated favicon/icon URLs
-type BrandingData struct {
- FaviconICO string // /.brand/{id}/favicon.ico
- Favicon16 string // /.brand/{id}/favicon-16x16.png
- Favicon32 string // /.brand/{id}/favicon-32x32.png
- Favicon96 string // /.brand/{id}/favicon-96x96.png
- AppleTouchIcon string // /.brand/{id}/apple-touch-icon.png (180x180)
- Android192 string // /.brand/{id}/android-chrome-192x192.png
- Android512 string // /.brand/{id}/android-chrome-512x512.png
- Maskable512 string // /.brand/{id}/maskable-512x512.png
- Master1024 string // /.brand/{id}/icon-1024x1024.png
- ManifestURL string // /.brand/{id}/site.webmanifest
- MaskIcon string // /.brand/{id}/mask-icon.svg
- MSTile150 string // /.brand/{id}/mstile-150x150.png
- BrowserConfig string // /.brand/{id}/browserconfig.xml
- ThemeColor string
- SVG string // Optional SVG pass-through
- IsGenerated bool
-}
-
-// CustomScriptEntry represents a single custom script with placement control
-type CustomScriptEntry struct {
- Name string
- Placement string // "head" or "body"
- Code string
- Enabled bool
-}
-
-// SiteSettingsData contains site-wide settings for head injection
-type SiteSettingsData struct {
- Title string
- Description string
- Favicon string
- Logo string
- LogoAlt string
- AppleTouchIcon string
- GoogleAnalyticsID string
- CustomScripts []CustomScriptEntry
- GoogleAnalyticsEnabled bool
- GoogleAnalyticsExplicitlySet bool // true if the enabled flag was explicitly set in JSON (not nil)
- MetaDescription string
- DefaultOGImage string
- TwitterHandle string
- OGSiteName string
- Branding BrandingData
- AdminBypassMode string // "maintenance", "coming_soon", or "" if not bypassing
- Toolbar ToolbarData
- LLMsTxtEnabled bool // Whether llms.txt is enabled for AI content discovery
- RSSFeedURL string // URL to the RSS feed (e.g., "/rss"), empty to disable
- RSSFeedTitle string // Feed title for discovery link
-}
-
-// PageMeta contains page-level SEO meta data (overrides site defaults)
-type PageMeta struct {
- MetaTitle string // Custom SEO title (overrides page title)
- MetaDescription string // Page-specific meta description
- OGTitle string // Open Graph title
- OGDescription string // Open Graph description
- OGImage string // Open Graph image URL
- TwitterTitle string // Twitter card title
- TwitterDescription string // Twitter card description
- TwitterImage string // Twitter card image URL
- CanonicalURL string // Canonical URL for this page
- RobotsDirective string // Robots directive (e.g., "noindex, nofollow")
- OGType string // Open Graph type ("article" for posts, else "website")
- PageURL string // Absolute URL of this page (og:url + auto-canonical)
- ArticlePublishedTime string // ISO 8601 published time (articles only)
- ArticleAuthor string // Author name (articles only)
-}
-
-// HeadData contains all data needed to render the element
-type HeadData struct {
- Title string
- Settings SiteSettingsData
- PageMeta PageMeta // Page-level SEO overrides
- ThemeCSS string
- ThemeMode string // "light", "dark", or "system"
- PluginStyles []string // Additional stylesheet URLs
- StructuredData string // JSON-LD structured data
- CSSHash string // Cache-busting hash for styles.css
- PageviewNonce string // Unique nonce for pageview deduplication
- EngagementConfig EngagementConfig // Engagement tracking config (for blog posts)
-}
-
-// ParseEngagementConfig extracts engagement config from the document map
-func ParseEngagementConfig(doc map[string]any) EngagementConfig {
- config := EngagementConfig{}
-
- engData, ok := doc["engagement_config"].(map[string]any)
- if !ok {
- return config
- }
-
- if v, ok := engData["page_path"].(string); ok {
- config.PagePath = v
- }
- if v, ok := engData["page_id"].(string); ok {
- config.PageID = v
- }
- if v, ok := engData["post_word_count"].(int); ok {
- config.PostWordCount = v
- }
- if v, ok := engData["session_id"].(string); ok {
- config.SessionID = v
- }
- if v, ok := engData["visitor_hash"].(string); ok {
- config.VisitorHash = v
- }
- if v, ok := engData["is_post"].(bool); ok {
- config.IsPost = v
- }
-
- return config
-}
-
-// ParseSiteSettings extracts site settings from the document map
-func ParseSiteSettings(doc map[string]any) SiteSettingsData {
- settings := SiteSettingsData{}
-
- siteData, ok := doc["site_settings"].(map[string]any)
- if !ok {
- return settings
- }
-
- if v, ok := siteData["title"].(string); ok {
- settings.Title = v
- }
- if v, ok := siteData["description"].(string); ok {
- settings.Description = v
- }
- if v, ok := siteData["favicon"].(string); ok {
- settings.Favicon = v
- }
- if v, ok := siteData["logo"].(string); ok {
- settings.Logo = v
- }
- if v, ok := siteData["logo_alt"].(string); ok {
- settings.LogoAlt = v
- }
- if v, ok := siteData["apple_touch_icon"].(string); ok {
- settings.AppleTouchIcon = v
- }
-
- // SEO defaults
- if seoData, ok := siteData["seo_defaults"].(map[string]any); ok {
- if v, ok := seoData["meta_description"].(string); ok {
- settings.MetaDescription = v
- }
- if v, ok := seoData["default_og_image"].(string); ok {
- settings.DefaultOGImage = v
- }
- if v, ok := seoData["twitter_handle"].(string); ok {
- settings.TwitterHandle = v
- }
- if v, ok := seoData["og_site_name"].(string); ok {
- settings.OGSiteName = v
- }
- }
-
- // Analytics
- var legacyHeadScripts, legacyBodyScripts string
- if analyticsData, ok := siteData["analytics"].(map[string]any); ok {
- if v, ok := analyticsData["google_analytics_id"].(string); ok {
- settings.GoogleAnalyticsID = v
- }
- // Read GA enabled flag — if present, use it; if absent but ID is set, default to enabled (backward compat)
- if v, ok := analyticsData["google_analytics_enabled"].(bool); ok {
- settings.GoogleAnalyticsEnabled = v
- settings.GoogleAnalyticsExplicitlySet = true
- } else if settings.GoogleAnalyticsID != "" {
- settings.GoogleAnalyticsEnabled = true
- }
- // Preserve legacy flat fields for fallback
- if v, ok := analyticsData["custom_head_scripts"].(string); ok {
- legacyHeadScripts = v
- }
- if v, ok := analyticsData["custom_body_scripts"].(string); ok {
- legacyBodyScripts = v
- }
- }
-
- // Structured custom scripts
- if scriptsArr, ok := siteData["custom_scripts"].([]any); ok && len(scriptsArr) > 0 {
- for _, item := range scriptsArr {
- entry, ok := item.(map[string]any)
- if !ok {
- continue
- }
- cs := CustomScriptEntry{}
- if v, ok := entry["name"].(string); ok {
- cs.Name = v
- }
- // Placement: proto enum stored as float64 (1=head, 2=body) or string
- switch p := entry["placement"].(type) {
- case float64:
- if p == 1 {
- cs.Placement = "head"
- } else if p == 2 {
- cs.Placement = "body"
- }
- case string:
- cs.Placement = p
- }
- if v, ok := entry["code"].(string); ok {
- cs.Code = v
- }
- if v, ok := entry["enabled"].(bool); ok {
- cs.Enabled = v
- }
- settings.CustomScripts = append(settings.CustomScripts, cs)
- }
- }
- // Fallback: if no structured scripts, migrate old flat fields
- if len(settings.CustomScripts) == 0 {
- if legacyHeadScripts != "" {
- settings.CustomScripts = append(settings.CustomScripts, CustomScriptEntry{
- Name: "Legacy Head Scripts",
- Placement: "head",
- Code: legacyHeadScripts,
- Enabled: true,
- })
- }
- if legacyBodyScripts != "" {
- settings.CustomScripts = append(settings.CustomScripts, CustomScriptEntry{
- Name: "Legacy Body Scripts",
- Placement: "body",
- Code: legacyBodyScripts,
- Enabled: true,
- })
- }
- }
-
- // Branding (generated favicons)
- if brandingData, ok := siteData["branding"].(map[string]any); ok {
- if v, ok := brandingData["is_generated"].(bool); ok {
- settings.Branding.IsGenerated = v
- }
- if v, ok := brandingData["favicon_ico"].(string); ok {
- settings.Branding.FaviconICO = v
- }
- if v, ok := brandingData["favicon_16"].(string); ok {
- settings.Branding.Favicon16 = v
- }
- if v, ok := brandingData["favicon_32"].(string); ok {
- settings.Branding.Favicon32 = v
- }
- if v, ok := brandingData["apple_touch_icon"].(string); ok {
- settings.Branding.AppleTouchIcon = v
- }
- if v, ok := brandingData["android_192"].(string); ok {
- settings.Branding.Android192 = v
- }
- if v, ok := brandingData["android_512"].(string); ok {
- settings.Branding.Android512 = v
- }
- if v, ok := brandingData["maskable_512"].(string); ok {
- settings.Branding.Maskable512 = v
- }
- if v, ok := brandingData["master_1024"].(string); ok {
- settings.Branding.Master1024 = v
- }
- if v, ok := brandingData["manifest_url"].(string); ok {
- settings.Branding.ManifestURL = v
- }
- if v, ok := brandingData["theme_color"].(string); ok {
- settings.Branding.ThemeColor = v
- }
- if v, ok := brandingData["svg"].(string); ok {
- settings.Branding.SVG = v
- }
- if v, ok := brandingData["favicon_96"].(string); ok {
- settings.Branding.Favicon96 = v
- }
- if v, ok := brandingData["mask_icon"].(string); ok {
- settings.Branding.MaskIcon = v
- }
- if v, ok := brandingData["mstile_150"].(string); ok {
- settings.Branding.MSTile150 = v
- }
- if v, ok := brandingData["browserconfig"].(string); ok {
- settings.Branding.BrowserConfig = v
- }
- }
-
- // Admin bypass mode (for showing banner when admin bypasses maintenance/coming_soon)
- if v, ok := siteData["admin_bypass_mode"].(string); ok {
- settings.AdminBypassMode = v
- }
-
- // Admin editor toolbar
- if toolbarData, ok := siteData["toolbar"].(map[string]any); ok {
- settings.Toolbar = ParseToolbarData(toolbarData)
- }
-
- // AI Optimization settings
- if aiOpt, ok := siteData["aiOptimization"].(map[string]any); ok {
- if v, ok := aiOpt["llmsTxtEnabled"].(bool); ok {
- settings.LLMsTxtEnabled = v
- }
- }
-
- // RSS feed auto-discovery (injected by page handler from system page)
- if v, ok := siteData["rss_feed_url"].(string); ok {
- settings.RSSFeedURL = v
- }
- if v, ok := siteData["rss_feed_title"].(string); ok {
- settings.RSSFeedTitle = v
- }
-
- return settings
-}
-
-// ParsePageMeta extracts page-level SEO metadata from the document map
-// These fields override site-level defaults when set
-func ParsePageMeta(doc map[string]any) PageMeta {
- meta := PageMeta{}
-
- // Page-level SEO fields are stored directly in the document root
- // (matching frontend PageInfo type in web/src/store/editor.ts)
- if v, ok := doc["metaTitle"].(string); ok {
- meta.MetaTitle = v
- }
- if v, ok := doc["metaDescription"].(string); ok {
- meta.MetaDescription = v
- }
- if v, ok := doc["ogTitle"].(string); ok {
- meta.OGTitle = v
- }
- if v, ok := doc["ogDescription"].(string); ok {
- meta.OGDescription = v
- }
- if v, ok := doc["ogImage"].(string); ok {
- meta.OGImage = v
- }
- if v, ok := doc["twitterTitle"].(string); ok {
- meta.TwitterTitle = v
- }
- if v, ok := doc["twitterDescription"].(string); ok {
- meta.TwitterDescription = v
- }
- if v, ok := doc["twitterImage"].(string); ok {
- meta.TwitterImage = v
- }
- if v, ok := doc["canonicalUrl"].(string); ok {
- meta.CanonicalURL = v
- }
- if v, ok := doc["robotsDirective"].(string); ok {
- meta.RobotsDirective = v
- }
- if v, ok := doc["ogType"].(string); ok {
- meta.OGType = v
- }
- if v, ok := doc["pageUrl"].(string); ok {
- meta.PageURL = v
- }
- if v, ok := doc["articlePublishedTime"].(string); ok {
- meta.ArticlePublishedTime = v
- }
- if v, ok := doc["articleAuthor"].(string); ok {
- meta.ArticleAuthor = v
- }
-
- return meta
-}
-
-// ParseToolbarData extracts toolbar data from the document map
-func ParseToolbarData(data map[string]any) ToolbarData {
- toolbar := ToolbarData{}
-
- if v, ok := data["enabled"].(bool); ok {
- toolbar.Enabled = v
- }
- if v, ok := data["page_id"].(string); ok {
- if id, err := uuid.Parse(v); err == nil {
- toolbar.PageID = id
- }
- }
- if v, ok := data["page_slug"].(string); ok {
- toolbar.PageSlug = v
- }
- if v, ok := data["page_title"].(string); ok {
- toolbar.PageTitle = v
- }
- if v, ok := data["post_type"].(string); ok {
- toolbar.PostType = v
- }
- if v, ok := data["status"].(string); ok {
- toolbar.Status = v
- }
- if v, ok := data["has_unpublished_changes"].(bool); ok {
- toolbar.HasUnpublishedChanges = v
- }
- if v, ok := data["preview_mode"].(string); ok {
- toolbar.PreviewMode = v
- }
- if v, ok := data["hide_preview_toggle"].(bool); ok {
- toolbar.HidePreviewToggle = v
- }
- if v, ok := data["animate"].(bool); ok {
- toolbar.Animate = v
- }
- if v, ok := data["position"].(string); ok {
- toolbar.Position = v
- }
- if v, ok := data["template_name"].(string); ok {
- toolbar.TemplateName = v
- }
- if v, ok := data["author_name"].(string); ok {
- toolbar.AuthorName = v
- }
- if v, ok := data["author_slug"].(string); ok {
- toolbar.AuthorSlug = v
- }
- if v, ok := data["last_modified"].(time.Time); ok {
- toolbar.LastModified = v
- }
- if v, ok := data["edit_url"].(string); ok {
- toolbar.EditURL = v
- }
- if v, ok := data["settings_url"].(string); ok {
- toolbar.SettingsURL = v
- }
- if v, ok := data["history_url"].(string); ok {
- toolbar.HistoryURL = v
- }
- if v, ok := data["analytics_url"].(string); ok {
- toolbar.AnalyticsURL = v
- }
-
- // Analytics snapshot
- if v, ok := data["today_pageviews"].(int64); ok {
- toolbar.TodayPageviews = v
- }
- if v, ok := data["pageviews_trend"].(string); ok {
- toolbar.PageviewsTrend = v
- }
- if v, ok := data["trend_percent"].(int); ok {
- toolbar.TrendPercent = v
- }
-
- // Blog-specific fields
- if v, ok := data["reading_time"].(int); ok {
- toolbar.ReadingTime = v
- }
- if v, ok := data["word_count"].(int); ok {
- toolbar.WordCount = v
- }
- if v, ok := data["category_count"].(int); ok {
- toolbar.CategoryCount = v
- }
-
- return toolbar
-}
-
-// EffectiveTitle returns the title to use in the tag.
-// Priority: PageMeta.MetaTitle → Page Title | Site Name → Page Title
-func (d HeadData) EffectiveTitle() string {
- if d.PageMeta.MetaTitle != "" {
- return d.PageMeta.MetaTitle
- }
- if d.Settings.OGSiteName != "" && d.Title != "" {
- return d.Title + " | " + d.Settings.OGSiteName
- }
- return d.Title
-}
-
-// Head renders the complete element with site settings and plugin extensions.
-templ Head(data HeadData) {
- @recordValidationCall(ctx, "Head")
-
-
-
- { data.EffectiveTitle() }
- if data.CSSHash != "" {
-
- } else {
-
- }
- @themeInitScript(data.ThemeMode)
-
- if data.Settings.Branding.IsGenerated {
- // Use generated brand assets (modern RFG-parity set; SVG first)
- if data.Settings.Branding.SVG != "" {
-
- }
- if data.Settings.Branding.Favicon96 != "" {
-
- }
-
-
-
-
- if data.Settings.Branding.MaskIcon != "" {
-
- }
-
- if data.Settings.Branding.ThemeColor != "" {
-
- }
- if data.Settings.Branding.BrowserConfig != "" {
-
- }
- if data.Title != "" {
-
- }
- } else {
- // Legacy fallback - manual favicon uploads
- if data.Settings.Favicon != "" {
-
- }
- if data.Settings.AppleTouchIcon != "" {
-
- }
- }
- // === SEO META TAGS ===
- // Fallback chain: Page-level → Site defaults
-
- // Meta description: Page override → Site default
- if data.PageMeta.MetaDescription != "" {
-
- } else if data.Settings.MetaDescription != "" {
-
- }
-
- // Canonical URL: admin override (resolved to absolute) → auto self-canonical (M8)
- if canonical := canonicalURL(data.PageMeta.CanonicalURL, data.PageMeta.PageURL); canonical != "" {
-
- }
-
- // Robots directive (page-level only - defaults to index,follow if not set)
- if data.PageMeta.RobotsDirective != "" {
-
- }
-
- // === OPEN GRAPH META TAGS ===
- // og:site_name is always site-level
- if data.Settings.OGSiteName != "" {
-
- }
-
- // og:title: Page OG → Page Meta Title → Page Title
- if data.PageMeta.OGTitle != "" {
-
- } else if data.PageMeta.MetaTitle != "" {
-
- } else if data.Title != "" {
-
- }
-
- // og:description: Page OG → Page Meta Description → Site default
- if data.PageMeta.OGDescription != "" {
-
- } else if data.PageMeta.MetaDescription != "" {
-
- } else if data.Settings.MetaDescription != "" {
-
- }
-
- // og:image: Page OG → Site default (resolve media: URLs)
- if data.PageMeta.OGImage != "" {
-
- } else if data.Settings.DefaultOGImage != "" {
-
- }
-
- // og:type: article for posts, website otherwise (M5)
- if data.PageMeta.OGType != "" {
-
- } else {
-
- }
- // og:url: absolute URL of this page (M6)
- if data.PageMeta.PageURL != "" {
-
- }
- // article:* metadata for posts (M5)
- if data.PageMeta.OGType == "article" {
- if data.PageMeta.ArticlePublishedTime != "" {
-
- }
- if data.PageMeta.ArticleAuthor != "" {
-
- }
- }
-
- // === TWITTER CARD META TAGS ===
- if data.Settings.TwitterHandle != "" {
-
- }
-
-
- // twitter:title: Page Twitter → Page OG → Page Meta Title → Page Title
- if data.PageMeta.TwitterTitle != "" {
-
- } else if data.PageMeta.OGTitle != "" {
-
- } else if data.PageMeta.MetaTitle != "" {
-
- } else if data.Title != "" {
-
- }
-
- // twitter:description: Page Twitter → Page OG → Page Meta Description → Site default
- if data.PageMeta.TwitterDescription != "" {
-
- } else if data.PageMeta.OGDescription != "" {
-
- } else if data.PageMeta.MetaDescription != "" {
-
- } else if data.Settings.MetaDescription != "" {
-
- }
-
- // twitter:image: Page Twitter → Page OG → Site default (resolve media: URLs)
- if data.PageMeta.TwitterImage != "" {
-
- } else if data.PageMeta.OGImage != "" {
-
- } else if data.Settings.DefaultOGImage != "" {
-
- }
-
- // === AI CONTENT DISCOVERY ===
- // llms.txt meta tag for AI crawlers (if enabled)
- if data.Settings.LLMsTxtEnabled {
-
-
- }
-
- // === RSS/ATOM FEED DISCOVERY ===
- if data.Settings.RSSFeedURL != "" {
-
-
- }
-
- if data.Settings.GoogleAnalyticsID != "" && (data.Settings.GoogleAnalyticsEnabled || !data.Settings.GoogleAnalyticsExplicitlySet) {
-
- @googleAnalyticsScript(data.Settings.GoogleAnalyticsID)
- }
- // Built-in analytics tracking (cookie-less) with nonce for server-side deduplication
- @analyticsScript(data.PageviewNonce)
- // Blog post engagement tracking (only rendered for posts with engagement config)
- @EngagementScript(data.EngagementConfig)
- for _, style := range data.PluginStyles {
-
- }
- // Theme CSS injected AFTER plugin styles to take precedence
- if data.ThemeCSS != "" {
- @themeStyle(data.ThemeCSS)
- }
- for _, script := range data.Settings.CustomScripts {
- if script.Enabled && script.Placement == "head" {
- @templ.Raw(script.Code)
- }
- }
- if data.StructuredData != "" {
- @structuredDataScript(data.StructuredData)
- }
- { children... }
-
-}
-
-// structuredDataScript renders JSON-LD structured data
-func structuredDataScript(jsonLD string) templ.Component {
- return templ.Raw(``)
-}
-
-// AdminBypassBanner renders a banner when admin is bypassing maintenance/coming_soon mode
-// This should be rendered at the very start of the to push down all content
-templ AdminBypassBanner(settings SiteSettingsData) {
- if settings.AdminBypassMode != "" {
-
-
-
- if settings.AdminBypassMode == "maintenance" {
-
- Maintenance Mode Active — You're seeing the normal site because you're logged in as admin.
- Manage
-
- } else if settings.AdminBypassMode == "coming_soon" {
-
- Coming Soon Mode Active — You're seeing the normal site because you're logged in as admin.
- Manage
-
- }
-
-
- }
-}
-
-// BodyEnd renders custom scripts and admin toolbar before
-templ BodyEnd(settings SiteSettingsData) {
- @recordValidationCall(ctx, "BodyEnd")
- for _, script := range settings.CustomScripts {
- if script.Enabled && script.Placement == "body" {
- @templ.Raw(script.Code)
- }
- }
- // Render admin editor toolbar if enabled (auto-injects for all templates)
- @AdminEditorToolbar(settings.Toolbar)
- { children... }
-}
-
-// themeStyle renders the theme CSS variables
-func themeStyleComponent(css string) templ.Component {
- return templ.Raw(``)
-}
-
-templ themeStyle(css string) {
- @themeStyleComponent(css)
-}
-
-// googleAnalyticsScript renders the GA4 inline script
-func googleAnalyticsScript(gaID string) templ.Component {
- return templ.Raw(``)
-}
-
-// analyticsScript renders the built-in analytics tracking script (cookie-less)
-// nonce is used for deduplication with server-side tracking
-templ analyticsScript(nonce string) {
-
-}
-
-func themeInitScript(themeMode string) templ.Component {
- switch themeMode {
- case "light", "dark", "system":
- default:
- themeMode = "light"
- }
- // Precedence: admin toolbar override (per-tab, sessionStorage) → visitor
- // preference (bn-theme cookie/localStorage) → site default → system.
- // Exposed as window.bnApplyTheme so the toolbar theme tester can re-apply
- // after changing the override without duplicating this resolution.
- return templ.Raw(``)
-}
diff --git a/templates/bn/head_templ.go b/templates/bn/head_templ.go
deleted file mode 100644
index d723d23..0000000
--- a/templates/bn/head_templ.go
+++ /dev/null
@@ -1,1761 +0,0 @@
-// Code generated by templ - DO NOT EDIT.
-
-// templ: version: v0.3.1020
-package bn
-
-//lint:file-ignore SA4006 This context is only used if a nested component is present.
-
-import "github.com/a-h/templ"
-import templruntime "github.com/a-h/templ/runtime"
-
-import (
- "strings"
- "time"
-
- "github.com/google/uuid"
-)
-
-// resolveMediaURL converts media: prefixed URLs to proper /media/ paths
-// e.g., "media:abc/image.webp" → "/media/abc/image.webp"
-func resolveMediaURL(url string) string {
- if strings.HasPrefix(url, "media:") {
- return "/media/" + strings.TrimPrefix(url, "media:")
- }
- return url
-}
-
-// canonicalURL resolves the canonical link. An admin-set value wins (made absolute
-// against the page's own origin when it is root-relative); otherwise the page's own
-// absolute URL is used as a self-referencing canonical. Returns "" only when neither
-// is available.
-func canonicalURL(canonical, pageURL string) string {
- if canonical == "" {
- return pageURL
- }
- if strings.HasPrefix(canonical, "http://") || strings.HasPrefix(canonical, "https://") {
- return canonical
- }
- if strings.HasPrefix(canonical, "/") && pageURL != "" {
- if i := strings.Index(pageURL, "://"); i >= 0 {
- if j := strings.IndexByte(pageURL[i+3:], '/'); j >= 0 {
- return pageURL[:i+3+j] + canonical
- }
- return pageURL + canonical
- }
- }
- return canonical
-}
-
-// BrandingData contains generated favicon/icon URLs
-type BrandingData struct {
- FaviconICO string // /.brand/{id}/favicon.ico
- Favicon16 string // /.brand/{id}/favicon-16x16.png
- Favicon32 string // /.brand/{id}/favicon-32x32.png
- Favicon96 string // /.brand/{id}/favicon-96x96.png
- AppleTouchIcon string // /.brand/{id}/apple-touch-icon.png (180x180)
- Android192 string // /.brand/{id}/android-chrome-192x192.png
- Android512 string // /.brand/{id}/android-chrome-512x512.png
- Maskable512 string // /.brand/{id}/maskable-512x512.png
- Master1024 string // /.brand/{id}/icon-1024x1024.png
- ManifestURL string // /.brand/{id}/site.webmanifest
- MaskIcon string // /.brand/{id}/mask-icon.svg
- MSTile150 string // /.brand/{id}/mstile-150x150.png
- BrowserConfig string // /.brand/{id}/browserconfig.xml
- ThemeColor string
- SVG string // Optional SVG pass-through
- IsGenerated bool
-}
-
-// CustomScriptEntry represents a single custom script with placement control
-type CustomScriptEntry struct {
- Name string
- Placement string // "head" or "body"
- Code string
- Enabled bool
-}
-
-// SiteSettingsData contains site-wide settings for head injection
-type SiteSettingsData struct {
- Title string
- Description string
- Favicon string
- Logo string
- LogoAlt string
- AppleTouchIcon string
- GoogleAnalyticsID string
- CustomScripts []CustomScriptEntry
- GoogleAnalyticsEnabled bool
- GoogleAnalyticsExplicitlySet bool // true if the enabled flag was explicitly set in JSON (not nil)
- MetaDescription string
- DefaultOGImage string
- TwitterHandle string
- OGSiteName string
- Branding BrandingData
- AdminBypassMode string // "maintenance", "coming_soon", or "" if not bypassing
- Toolbar ToolbarData
- LLMsTxtEnabled bool // Whether llms.txt is enabled for AI content discovery
- RSSFeedURL string // URL to the RSS feed (e.g., "/rss"), empty to disable
- RSSFeedTitle string // Feed title for discovery link
-}
-
-// PageMeta contains page-level SEO meta data (overrides site defaults)
-type PageMeta struct {
- MetaTitle string // Custom SEO title (overrides page title)
- MetaDescription string // Page-specific meta description
- OGTitle string // Open Graph title
- OGDescription string // Open Graph description
- OGImage string // Open Graph image URL
- TwitterTitle string // Twitter card title
- TwitterDescription string // Twitter card description
- TwitterImage string // Twitter card image URL
- CanonicalURL string // Canonical URL for this page
- RobotsDirective string // Robots directive (e.g., "noindex, nofollow")
- OGType string // Open Graph type ("article" for posts, else "website")
- PageURL string // Absolute URL of this page (og:url + auto-canonical)
- ArticlePublishedTime string // ISO 8601 published time (articles only)
- ArticleAuthor string // Author name (articles only)
-}
-
-// HeadData contains all data needed to render the element
-type HeadData struct {
- Title string
- Settings SiteSettingsData
- PageMeta PageMeta // Page-level SEO overrides
- ThemeCSS string
- ThemeMode string // "light", "dark", or "system"
- PluginStyles []string // Additional stylesheet URLs
- StructuredData string // JSON-LD structured data
- CSSHash string // Cache-busting hash for styles.css
- PageviewNonce string // Unique nonce for pageview deduplication
- EngagementConfig EngagementConfig // Engagement tracking config (for blog posts)
-}
-
-// ParseEngagementConfig extracts engagement config from the document map
-func ParseEngagementConfig(doc map[string]any) EngagementConfig {
- config := EngagementConfig{}
-
- engData, ok := doc["engagement_config"].(map[string]any)
- if !ok {
- return config
- }
-
- if v, ok := engData["page_path"].(string); ok {
- config.PagePath = v
- }
- if v, ok := engData["page_id"].(string); ok {
- config.PageID = v
- }
- if v, ok := engData["post_word_count"].(int); ok {
- config.PostWordCount = v
- }
- if v, ok := engData["session_id"].(string); ok {
- config.SessionID = v
- }
- if v, ok := engData["visitor_hash"].(string); ok {
- config.VisitorHash = v
- }
- if v, ok := engData["is_post"].(bool); ok {
- config.IsPost = v
- }
-
- return config
-}
-
-// ParseSiteSettings extracts site settings from the document map
-func ParseSiteSettings(doc map[string]any) SiteSettingsData {
- settings := SiteSettingsData{}
-
- siteData, ok := doc["site_settings"].(map[string]any)
- if !ok {
- return settings
- }
-
- if v, ok := siteData["title"].(string); ok {
- settings.Title = v
- }
- if v, ok := siteData["description"].(string); ok {
- settings.Description = v
- }
- if v, ok := siteData["favicon"].(string); ok {
- settings.Favicon = v
- }
- if v, ok := siteData["logo"].(string); ok {
- settings.Logo = v
- }
- if v, ok := siteData["logo_alt"].(string); ok {
- settings.LogoAlt = v
- }
- if v, ok := siteData["apple_touch_icon"].(string); ok {
- settings.AppleTouchIcon = v
- }
-
- // SEO defaults
- if seoData, ok := siteData["seo_defaults"].(map[string]any); ok {
- if v, ok := seoData["meta_description"].(string); ok {
- settings.MetaDescription = v
- }
- if v, ok := seoData["default_og_image"].(string); ok {
- settings.DefaultOGImage = v
- }
- if v, ok := seoData["twitter_handle"].(string); ok {
- settings.TwitterHandle = v
- }
- if v, ok := seoData["og_site_name"].(string); ok {
- settings.OGSiteName = v
- }
- }
-
- // Analytics
- var legacyHeadScripts, legacyBodyScripts string
- if analyticsData, ok := siteData["analytics"].(map[string]any); ok {
- if v, ok := analyticsData["google_analytics_id"].(string); ok {
- settings.GoogleAnalyticsID = v
- }
- // Read GA enabled flag — if present, use it; if absent but ID is set, default to enabled (backward compat)
- if v, ok := analyticsData["google_analytics_enabled"].(bool); ok {
- settings.GoogleAnalyticsEnabled = v
- settings.GoogleAnalyticsExplicitlySet = true
- } else if settings.GoogleAnalyticsID != "" {
- settings.GoogleAnalyticsEnabled = true
- }
- // Preserve legacy flat fields for fallback
- if v, ok := analyticsData["custom_head_scripts"].(string); ok {
- legacyHeadScripts = v
- }
- if v, ok := analyticsData["custom_body_scripts"].(string); ok {
- legacyBodyScripts = v
- }
- }
-
- // Structured custom scripts
- if scriptsArr, ok := siteData["custom_scripts"].([]any); ok && len(scriptsArr) > 0 {
- for _, item := range scriptsArr {
- entry, ok := item.(map[string]any)
- if !ok {
- continue
- }
- cs := CustomScriptEntry{}
- if v, ok := entry["name"].(string); ok {
- cs.Name = v
- }
- // Placement: proto enum stored as float64 (1=head, 2=body) or string
- switch p := entry["placement"].(type) {
- case float64:
- if p == 1 {
- cs.Placement = "head"
- } else if p == 2 {
- cs.Placement = "body"
- }
- case string:
- cs.Placement = p
- }
- if v, ok := entry["code"].(string); ok {
- cs.Code = v
- }
- if v, ok := entry["enabled"].(bool); ok {
- cs.Enabled = v
- }
- settings.CustomScripts = append(settings.CustomScripts, cs)
- }
- }
- // Fallback: if no structured scripts, migrate old flat fields
- if len(settings.CustomScripts) == 0 {
- if legacyHeadScripts != "" {
- settings.CustomScripts = append(settings.CustomScripts, CustomScriptEntry{
- Name: "Legacy Head Scripts",
- Placement: "head",
- Code: legacyHeadScripts,
- Enabled: true,
- })
- }
- if legacyBodyScripts != "" {
- settings.CustomScripts = append(settings.CustomScripts, CustomScriptEntry{
- Name: "Legacy Body Scripts",
- Placement: "body",
- Code: legacyBodyScripts,
- Enabled: true,
- })
- }
- }
-
- // Branding (generated favicons)
- if brandingData, ok := siteData["branding"].(map[string]any); ok {
- if v, ok := brandingData["is_generated"].(bool); ok {
- settings.Branding.IsGenerated = v
- }
- if v, ok := brandingData["favicon_ico"].(string); ok {
- settings.Branding.FaviconICO = v
- }
- if v, ok := brandingData["favicon_16"].(string); ok {
- settings.Branding.Favicon16 = v
- }
- if v, ok := brandingData["favicon_32"].(string); ok {
- settings.Branding.Favicon32 = v
- }
- if v, ok := brandingData["apple_touch_icon"].(string); ok {
- settings.Branding.AppleTouchIcon = v
- }
- if v, ok := brandingData["android_192"].(string); ok {
- settings.Branding.Android192 = v
- }
- if v, ok := brandingData["android_512"].(string); ok {
- settings.Branding.Android512 = v
- }
- if v, ok := brandingData["maskable_512"].(string); ok {
- settings.Branding.Maskable512 = v
- }
- if v, ok := brandingData["master_1024"].(string); ok {
- settings.Branding.Master1024 = v
- }
- if v, ok := brandingData["manifest_url"].(string); ok {
- settings.Branding.ManifestURL = v
- }
- if v, ok := brandingData["theme_color"].(string); ok {
- settings.Branding.ThemeColor = v
- }
- if v, ok := brandingData["svg"].(string); ok {
- settings.Branding.SVG = v
- }
- if v, ok := brandingData["favicon_96"].(string); ok {
- settings.Branding.Favicon96 = v
- }
- if v, ok := brandingData["mask_icon"].(string); ok {
- settings.Branding.MaskIcon = v
- }
- if v, ok := brandingData["mstile_150"].(string); ok {
- settings.Branding.MSTile150 = v
- }
- if v, ok := brandingData["browserconfig"].(string); ok {
- settings.Branding.BrowserConfig = v
- }
- }
-
- // Admin bypass mode (for showing banner when admin bypasses maintenance/coming_soon)
- if v, ok := siteData["admin_bypass_mode"].(string); ok {
- settings.AdminBypassMode = v
- }
-
- // Admin editor toolbar
- if toolbarData, ok := siteData["toolbar"].(map[string]any); ok {
- settings.Toolbar = ParseToolbarData(toolbarData)
- }
-
- // AI Optimization settings
- if aiOpt, ok := siteData["aiOptimization"].(map[string]any); ok {
- if v, ok := aiOpt["llmsTxtEnabled"].(bool); ok {
- settings.LLMsTxtEnabled = v
- }
- }
-
- // RSS feed auto-discovery (injected by page handler from system page)
- if v, ok := siteData["rss_feed_url"].(string); ok {
- settings.RSSFeedURL = v
- }
- if v, ok := siteData["rss_feed_title"].(string); ok {
- settings.RSSFeedTitle = v
- }
-
- return settings
-}
-
-// ParsePageMeta extracts page-level SEO metadata from the document map
-// These fields override site-level defaults when set
-func ParsePageMeta(doc map[string]any) PageMeta {
- meta := PageMeta{}
-
- // Page-level SEO fields are stored directly in the document root
- // (matching frontend PageInfo type in web/src/store/editor.ts)
- if v, ok := doc["metaTitle"].(string); ok {
- meta.MetaTitle = v
- }
- if v, ok := doc["metaDescription"].(string); ok {
- meta.MetaDescription = v
- }
- if v, ok := doc["ogTitle"].(string); ok {
- meta.OGTitle = v
- }
- if v, ok := doc["ogDescription"].(string); ok {
- meta.OGDescription = v
- }
- if v, ok := doc["ogImage"].(string); ok {
- meta.OGImage = v
- }
- if v, ok := doc["twitterTitle"].(string); ok {
- meta.TwitterTitle = v
- }
- if v, ok := doc["twitterDescription"].(string); ok {
- meta.TwitterDescription = v
- }
- if v, ok := doc["twitterImage"].(string); ok {
- meta.TwitterImage = v
- }
- if v, ok := doc["canonicalUrl"].(string); ok {
- meta.CanonicalURL = v
- }
- if v, ok := doc["robotsDirective"].(string); ok {
- meta.RobotsDirective = v
- }
- if v, ok := doc["ogType"].(string); ok {
- meta.OGType = v
- }
- if v, ok := doc["pageUrl"].(string); ok {
- meta.PageURL = v
- }
- if v, ok := doc["articlePublishedTime"].(string); ok {
- meta.ArticlePublishedTime = v
- }
- if v, ok := doc["articleAuthor"].(string); ok {
- meta.ArticleAuthor = v
- }
-
- return meta
-}
-
-// ParseToolbarData extracts toolbar data from the document map
-func ParseToolbarData(data map[string]any) ToolbarData {
- toolbar := ToolbarData{}
-
- if v, ok := data["enabled"].(bool); ok {
- toolbar.Enabled = v
- }
- if v, ok := data["page_id"].(string); ok {
- if id, err := uuid.Parse(v); err == nil {
- toolbar.PageID = id
- }
- }
- if v, ok := data["page_slug"].(string); ok {
- toolbar.PageSlug = v
- }
- if v, ok := data["page_title"].(string); ok {
- toolbar.PageTitle = v
- }
- if v, ok := data["post_type"].(string); ok {
- toolbar.PostType = v
- }
- if v, ok := data["status"].(string); ok {
- toolbar.Status = v
- }
- if v, ok := data["has_unpublished_changes"].(bool); ok {
- toolbar.HasUnpublishedChanges = v
- }
- if v, ok := data["preview_mode"].(string); ok {
- toolbar.PreviewMode = v
- }
- if v, ok := data["hide_preview_toggle"].(bool); ok {
- toolbar.HidePreviewToggle = v
- }
- if v, ok := data["animate"].(bool); ok {
- toolbar.Animate = v
- }
- if v, ok := data["position"].(string); ok {
- toolbar.Position = v
- }
- if v, ok := data["template_name"].(string); ok {
- toolbar.TemplateName = v
- }
- if v, ok := data["author_name"].(string); ok {
- toolbar.AuthorName = v
- }
- if v, ok := data["author_slug"].(string); ok {
- toolbar.AuthorSlug = v
- }
- if v, ok := data["last_modified"].(time.Time); ok {
- toolbar.LastModified = v
- }
- if v, ok := data["edit_url"].(string); ok {
- toolbar.EditURL = v
- }
- if v, ok := data["settings_url"].(string); ok {
- toolbar.SettingsURL = v
- }
- if v, ok := data["history_url"].(string); ok {
- toolbar.HistoryURL = v
- }
- if v, ok := data["analytics_url"].(string); ok {
- toolbar.AnalyticsURL = v
- }
-
- // Analytics snapshot
- if v, ok := data["today_pageviews"].(int64); ok {
- toolbar.TodayPageviews = v
- }
- if v, ok := data["pageviews_trend"].(string); ok {
- toolbar.PageviewsTrend = v
- }
- if v, ok := data["trend_percent"].(int); ok {
- toolbar.TrendPercent = v
- }
-
- // Blog-specific fields
- if v, ok := data["reading_time"].(int); ok {
- toolbar.ReadingTime = v
- }
- if v, ok := data["word_count"].(int); ok {
- toolbar.WordCount = v
- }
- if v, ok := data["category_count"].(int); ok {
- toolbar.CategoryCount = v
- }
-
- return toolbar
-}
-
-// EffectiveTitle returns the title to use in the tag.
-// Priority: PageMeta.MetaTitle → Page Title | Site Name → Page Title
-func (d HeadData) EffectiveTitle() string {
- if d.PageMeta.MetaTitle != "" {
- return d.PageMeta.MetaTitle
- }
- if d.Settings.OGSiteName != "" && d.Title != "" {
- return d.Title + " | " + d.Settings.OGSiteName
- }
- return d.Title
-}
-
-// Head renders the complete element with site settings and plugin extensions.
-func Head(data HeadData) 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 {
- return templ_7745c5c3_CtxErr
- }
- templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
- if !templ_7745c5c3_IsBuffer {
- defer func() {
- templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err == nil {
- templ_7745c5c3_Err = templ_7745c5c3_BufErr
- }
- }()
- }
- ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var1 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var1 == nil {
- templ_7745c5c3_Var1 = templ.NopComponent
- }
- ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = recordValidationCall(ctx, "Head").Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- var templ_7745c5c3_Var2 string
- templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.EffectiveTitle())
- if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `head.templ`, Line: 513, Col: 32}
- }
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.CSSHash != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = themeInitScript(data.ThemeMode).Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.Settings.Branding.IsGenerated {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.Settings.Branding.SVG != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.Settings.Branding.Favicon96 != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.Settings.Branding.MaskIcon != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.Settings.Branding.ThemeColor != "" {
- 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, 25, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.Settings.Branding.BrowserConfig != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.Title != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.Settings.Favicon != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.Settings.AppleTouchIcon != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- }
- if data.PageMeta.MetaDescription != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.Settings.MetaDescription != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if canonical := canonicalURL(data.PageMeta.CanonicalURL, data.PageMeta.PageURL); canonical != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.PageMeta.RobotsDirective != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.Settings.OGSiteName != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.PageMeta.OGTitle != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.PageMeta.MetaTitle != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.Title != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.PageMeta.OGDescription != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.PageMeta.MetaDescription != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.Settings.MetaDescription != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.PageMeta.OGImage != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.Settings.DefaultOGImage != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.PageMeta.OGType != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.PageMeta.PageURL != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.PageMeta.OGType == "article" {
- if data.PageMeta.ArticlePublishedTime != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.PageMeta.ArticleAuthor != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- }
- if data.Settings.TwitterHandle != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.PageMeta.TwitterTitle != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.PageMeta.OGTitle != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.PageMeta.MetaTitle != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.Title != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.PageMeta.TwitterDescription != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.PageMeta.OGDescription != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.PageMeta.MetaDescription != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.Settings.MetaDescription != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.PageMeta.TwitterImage != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.PageMeta.OGImage != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if data.Settings.DefaultOGImage != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.Settings.LLMsTxtEnabled {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.Settings.RSSFeedURL != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.Settings.GoogleAnalyticsID != "" && (data.Settings.GoogleAnalyticsEnabled || !data.Settings.GoogleAnalyticsExplicitlySet) {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = googleAnalyticsScript(data.Settings.GoogleAnalyticsID).Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = analyticsScript(data.PageviewNonce).Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = EngagementScript(data.EngagementConfig).Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- for _, style := range data.PluginStyles {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- if data.ThemeCSS != "" {
- templ_7745c5c3_Err = themeStyle(data.ThemeCSS).Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- for _, script := range data.Settings.CustomScripts {
- if script.Enabled && script.Placement == "head" {
- templ_7745c5c3_Err = templ.Raw(script.Code).Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- }
- if data.StructuredData != "" {
- templ_7745c5c3_Err = structuredDataScript(data.StructuredData).Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- return nil
- })
-}
-
-// structuredDataScript renders JSON-LD structured data
-func structuredDataScript(jsonLD string) templ.Component {
- return templ.Raw(``)
-}
-
-// AdminBypassBanner renders a banner when admin is bypassing maintenance/coming_soon mode
-// This should be rendered at the very start of the to push down all content
-func AdminBypassBanner(settings SiteSettingsData) 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 {
- return templ_7745c5c3_CtxErr
- }
- templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
- if !templ_7745c5c3_IsBuffer {
- defer func() {
- templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err == nil {
- templ_7745c5c3_Err = templ_7745c5c3_BufErr
- }
- }()
- }
- ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var53 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var53 == nil {
- templ_7745c5c3_Var53 = templ.NopComponent
- }
- ctx = templ.ClearChildren(ctx)
- if settings.AdminBypassMode != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if settings.AdminBypassMode == "maintenance" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "Maintenance Mode Active — You're seeing the normal site because you're logged in as admin. Manage")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else if settings.AdminBypassMode == "coming_soon" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "Coming Soon Mode Active — You're seeing the normal site because you're logged in as admin. Manage")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- return nil
- })
-}
-
-// BodyEnd renders custom scripts and admin toolbar before
-func BodyEnd(settings SiteSettingsData) 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 {
- return templ_7745c5c3_CtxErr
- }
- templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
- if !templ_7745c5c3_IsBuffer {
- defer func() {
- templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err == nil {
- templ_7745c5c3_Err = templ_7745c5c3_BufErr
- }
- }()
- }
- ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var54 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var54 == nil {
- templ_7745c5c3_Var54 = templ.NopComponent
- }
- ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = recordValidationCall(ctx, "BodyEnd").Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- for _, script := range settings.CustomScripts {
- if script.Enabled && script.Placement == "body" {
- templ_7745c5c3_Err = templ.Raw(script.Code).Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- }
- templ_7745c5c3_Err = AdminEditorToolbar(settings.Toolbar).Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = templ_7745c5c3_Var54.Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- return nil
- })
-}
-
-// themeStyle renders the theme CSS variables
-func themeStyleComponent(css string) templ.Component {
- return templ.Raw(``)
-}
-
-func themeStyle(css string) 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 {
- return templ_7745c5c3_CtxErr
- }
- templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
- if !templ_7745c5c3_IsBuffer {
- defer func() {
- templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err == nil {
- templ_7745c5c3_Err = templ_7745c5c3_BufErr
- }
- }()
- }
- ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var55 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var55 == nil {
- templ_7745c5c3_Var55 = templ.NopComponent
- }
- ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = themeStyleComponent(css).Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- return nil
- })
-}
-
-// googleAnalyticsScript renders the GA4 inline script
-func googleAnalyticsScript(gaID string) templ.Component {
- return templ.Raw(``)
-}
-
-// analyticsScript renders the built-in analytics tracking script (cookie-less)
-// nonce is used for deduplication with server-side tracking
-func analyticsScript(nonce string) 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 {
- return templ_7745c5c3_CtxErr
- }
- templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
- if !templ_7745c5c3_IsBuffer {
- defer func() {
- templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err == nil {
- templ_7745c5c3_Err = templ_7745c5c3_BufErr
- }
- }()
- }
- ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var56 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var56 == nil {
- templ_7745c5c3_Var56 = templ.NopComponent
- }
- ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- return nil
- })
-}
-
-func themeInitScript(themeMode string) templ.Component {
- switch themeMode {
- case "light", "dark", "system":
- default:
- themeMode = "light"
- }
- // Precedence: admin toolbar override (per-tab, sessionStorage) → visitor
- // preference (bn-theme cookie/localStorage) → site default → system.
- // Exposed as window.bnApplyTheme so the toolbar theme tester can re-apply
- // after changing the override without duplicating this resolution.
- return templ.Raw(``)
-}
-
-var _ = templruntime.GeneratedTemplate
diff --git a/templates/bn/toolbar.templ b/templates/bn/toolbar.templ
deleted file mode 100644
index 05d32aa..0000000
--- a/templates/bn/toolbar.templ
+++ /dev/null
@@ -1,686 +0,0 @@
-package bn
-
-import (
- "fmt"
- "time"
-
- "github.com/google/uuid"
-)
-
-// ToolbarData contains data for the admin editor toolbar
-type ToolbarData struct {
- Enabled bool
- PageID uuid.UUID
- PageSlug string
- PageTitle string
- PostType string // "page", "post", "master", "system"
- Status string // "published", "draft", "scheduled"
- HasUnpublishedChanges bool
- PreviewMode string // "published" or "draft"
- HidePreviewToggle bool
- Animate bool // true only on initial full-page render (entrance animation); false on HTMX re-renders
- Position string // "tl", "tc", "tr", "bl", "bc", "br" (default: "tr")
- ScheduledAt *time.Time
- TemplateName string
- AuthorName string
- LastModified time.Time
- EditURL string
- SettingsURL string
- HistoryURL string
- AnalyticsURL string
-
- // Analytics snapshot
- TodayPageviews int64
- PageviewsTrend string // "up", "down", "flat"
- TrendPercent int
-
- // Blog-specific fields
- ReadingTime int // minutes
- WordCount int
- CategoryCount int
- AuthorSlug string
-}
-
-// StatusBadgeClass returns the Tailwind classes for the status badge
-func (t ToolbarData) StatusBadgeClass() string {
- switch t.Status {
- case "published":
- return "bg-success text-success-foreground"
- case "scheduled":
- return "bg-info text-info-foreground"
- default:
- return "bg-warning text-warning-foreground"
- }
-}
-
-// StatusLabel returns a human-readable status label
-func (t ToolbarData) StatusLabel() string {
- switch t.Status {
- case "published":
- return "Published"
- case "scheduled":
- return "Scheduled"
- default:
- return "Draft"
- }
-}
-
-// PostTypeLabel returns a human-readable post type label
-func (t ToolbarData) PostTypeLabel() string {
- switch t.PostType {
- case "post":
- return "Post"
- case "master":
- return "Master"
- case "system":
- return "System"
- default:
- return "Page"
- }
-}
-
-// IsPreviewMode returns true if currently viewing draft/preview
-func (t ToolbarData) IsPreviewMode() bool {
- return t.PreviewMode == "draft"
-}
-
-// CanPublish returns true if the publish button should be enabled
-func (t ToolbarData) CanPublish() bool {
- return t.HasUnpublishedChanges
-}
-
-// LastModifiedFormatted returns the last modified time in a readable format
-func (t ToolbarData) LastModifiedFormatted() string {
- return t.LastModified.Format("Jan 2, 2006 3:04 PM")
-}
-
-// IsTopPosition returns true if toolbar should be at top (for legacy compatibility)
-func (t ToolbarData) IsTopPosition() bool {
- return t.Position == "tl" || t.Position == "tc" || t.Position == "tr" || t.Position == ""
-}
-
-// IsBottomPosition returns true if toolbar is at bottom
-func (t ToolbarData) IsBottomPosition() bool {
- return t.Position == "bl" || t.Position == "bc" || t.Position == "br"
-}
-
-// IsLeftPosition returns true if toolbar is on the left side
-func (t ToolbarData) IsLeftPosition() bool {
- return t.Position == "tl" || t.Position == "bl"
-}
-
-// IsRightPosition returns true if toolbar is on the right side
-func (t ToolbarData) IsRightPosition() bool {
- return t.Position == "tr" || t.Position == "br" || t.Position == ""
-}
-
-// IsCenterPosition returns true if toolbar is centered
-func (t ToolbarData) IsCenterPosition() bool {
- return t.Position == "tc" || t.Position == "bc"
-}
-
-// DropdownDirection returns "up" or "down" based on toolbar vertical position
-func (t ToolbarData) DropdownDirection() string {
- if t.IsBottomPosition() {
- return "up"
- }
- return "down"
-}
-
-// DropdownAlign returns "left", "center", or "right" based on toolbar horizontal position
-func (t ToolbarData) DropdownAlign() string {
- if t.IsLeftPosition() {
- return "left"
- } else if t.IsCenterPosition() {
- return "center"
- }
- return "right"
-}
-
-// EnterAnimationClass returns the entrance-animation classes for the initial
-// full-page render. It slides the pill in from the edge it rests against (up for
-// bottom positions, down for top) then gives a brief pulse. Empty on HTMX
-// re-renders (Animate=false) so publish/discard/reposition swaps appear instantly.
-func (t ToolbarData) EnterAnimationClass() string {
- if !t.Animate {
- return ""
- }
- if t.IsBottomPosition() {
- return "bn-toolbar-enter bn-toolbar-enter-up"
- }
- return "bn-toolbar-enter bn-toolbar-enter-down"
-}
-
-// PositionClasses returns the positioning classes for the floating pill
-func (t ToolbarData) PositionClasses() string {
- switch t.Position {
- case "tl":
- return "top-4 left-4"
- case "tc":
- return "top-4 left-1/2 -translate-x-1/2"
- case "bl":
- return "bottom-4 left-4"
- case "bc":
- return "bottom-4 left-1/2 -translate-x-1/2"
- case "br":
- return "bottom-4 right-4"
- default: // "tr" or empty
- return "top-4 right-4"
- }
-}
-
-// IsBlogPost returns true if this is a blog post
-func (t ToolbarData) IsBlogPost() bool {
- return t.PostType == "post"
-}
-
-// TrendIcon returns the trend icon for pageviews
-func (t ToolbarData) TrendIcon() string {
- switch t.PageviewsTrend {
- case "up":
- return "↑"
- case "down":
- return "↓"
- default:
- return "→"
- }
-}
-
-// TrendColorClass returns the color class for the trend indicator
-func (t ToolbarData) TrendColorClass() string {
- switch t.PageviewsTrend {
- case "up":
- return "text-success"
- case "down":
- return "text-destructive"
- default:
- return "text-muted-foreground"
- }
-}
-
-// AdminEditorToolbar renders the floating pill toolbar for admins on public pages
-// Uses inverted color scheme - dark on light backgrounds, light on dark backgrounds
-templ AdminEditorToolbar(data ToolbarData) {
- if data.Enabled {
-