From 27e57b8837cf8b7585c486c24772f749fe135841 Mon Sep 17 00:00:00 2001 From: Alex Dunmow Date: Tue, 7 Jul 2026 13:32:22 +0800 Subject: [PATCH] =?UTF-8?q?feat!:=20delete=20guest=20chrome=20(templates/b?= =?UTF-8?q?n,=20templates/pongo)=20=E2=80=94=20host=20composes=20the=20env?= =?UTF-8?q?elope=20(P3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- AGENTS.md | 14 +- templates/bn/asset_hooks.go | 11 - templates/bn/engagement.templ | 514 --------- templates/bn/engagement_templ.go | 80 -- templates/bn/head.templ | 961 ---------------- templates/bn/head_templ.go | 1761 ------------------------------ templates/bn/toolbar.templ | 686 ------------ templates/bn/toolbar_templ.go | 996 ----------------- templates/bn/validation.go | 50 - templates/pongo/base.html | 9 - templates/pongo/context.go | 100 -- templates/pongo/embed.go | 6 - templates/pongo/engine.go | 169 --- templates/pongo/loader.go | 43 - 14 files changed, 5 insertions(+), 5395 deletions(-) delete mode 100644 templates/bn/asset_hooks.go delete mode 100644 templates/bn/engagement.templ delete mode 100644 templates/bn/engagement_templ.go delete mode 100644 templates/bn/head.templ delete mode 100644 templates/bn/head_templ.go delete mode 100644 templates/bn/toolbar.templ delete mode 100644 templates/bn/toolbar_templ.go delete mode 100644 templates/bn/validation.go delete mode 100644 templates/pongo/base.html delete mode 100644 templates/pongo/context.go delete mode 100644 templates/pongo/embed.go delete mode 100644 templates/pongo/engine.go delete mode 100644 templates/pongo/loader.go 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 <head> element with site settings and plugin extensions. -templ Head(data HeadData) { - @recordValidationCall(ctx, "Head") - <head> - <meta charset="UTF-8"/> - <meta name="viewport" content="width=device-width, initial-scale=1.0"/> - <title>{ data.EffectiveTitle() } - if data.CSSHash != "" { - - } - @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) { - `) -} - -// 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 <head> 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, "<head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>") - 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 { -
- - - - - - - - - -
- - -
- if !data.HidePreviewToggle { - -
- - - } - -
- - - - - if data.IsBlogPost() && data.ReadingTime > 0 { - - } - - if data.CanPublish() { - - } - -
- -
-
-
- - - } -} - -// PageInfoDropdown renders the page info and quick actions dropdown -templ PageInfoDropdown(data ToolbarData) { -
- - - if data.TodayPageviews > 0 || data.PageviewsTrend != "" { -
-
-
- - - - { fmt.Sprintf("%d", data.TodayPageviews) } views today -
- if data.TrendPercent != 0 { - - { data.TrendIcon() } { fmt.Sprintf("%d%%", abs(data.TrendPercent)) } - - } -
-
- } - - if data.IsBlogPost() { -
-
- if data.AuthorName != "" { -
- - - - if data.AuthorSlug != "" { - { data.AuthorName } - } else { - { data.AuthorName } - } -
- } - if data.ReadingTime > 0 { - { fmt.Sprintf("%d min read", data.ReadingTime) } - } -
- if data.WordCount > 0 { -
{ fmt.Sprintf("%d words", data.WordCount) }
- } -
- } - -
-
- Template - { data.TemplateName } -
-
- Modified - { data.LastModifiedFormatted() } -
-
- - - -
- - -
- -
-
Move toolbar
-
- @positionButton(data.PageID, "tl", data.Position, "Top left") - @positionButton(data.PageID, "tc", data.Position, "Top center") - @positionButton(data.PageID, "tr", data.Position, "Top right") - @positionButton(data.PageID, "bl", data.Position, "Bottom left") - @positionButton(data.PageID, "bc", data.Position, "Bottom center") - @positionButton(data.PageID, "br", data.Position, "Bottom right") -
-
- - if data.HasUnpublishedChanges { -
- -
- } -
-} - -// positionButton renders a position selector button -templ positionButton(pageID uuid.UUID, pos string, currentPos string, label string) { - {{ active := pos == currentPos || (currentPos == "" && pos == "tr") }} - -} - -// abs returns the absolute value of an integer -func abs(n int) int { - if n < 0 { - return -n - } - return n -} - -// copyPageURL generates the script to copy page URL -script copyPageURL(slug string) { - const url = window.location.origin + slug; - navigator.clipboard.writeText(url).then(() => { - // Show brief feedback - could enhance with toast later - const btn = event.currentTarget; - const originalText = btn.innerHTML; - btn.innerHTML = ' Copied!'; - setTimeout(() => { btn.innerHTML = originalText; }, 2000); - }); -} - -// togglePreviewMode toggles between preview (draft) and published mode via URL -script togglePreviewMode(isCurrentlyPreview bool) { - const url = new URL(window.location.href); - if (isCurrentlyPreview) { - // Currently previewing, remove preview param - url.searchParams.delete('preview'); - } else { - // Not previewing, add preview param - url.searchParams.set('preview', '1'); - } - window.location.href = url.toString(); -} diff --git a/templates/bn/toolbar_templ.go b/templates/bn/toolbar_templ.go deleted file mode 100644 index 04c094a..0000000 --- a/templates/bn/toolbar_templ.go +++ /dev/null @@ -1,996 +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 ( - "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 -func AdminEditorToolbar(data ToolbarData) 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 data.Enabled { - var templ_7745c5c3_Var2 = []any{"bn-toolbar fixed z-[9999] flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-full backdrop-blur-md transition-all duration-300", data.PositionClasses(), data.EnterAnimationClass()} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Edit
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var6 = []any{"w-2 h-2 rounded-full", templ.KV("bg-success", data.Status == "published"), templ.KV("bg-warning", data.Status == "draft"), templ.KV("bg-info", data.Status == "scheduled"), templ.KV("animate-pulse ring-2 ring-warning/50", data.HasUnpublishedChanges)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !data.HidePreviewToggle { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var9 = []any{"bn-toolbar-hover inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs transition-colors", templ.KV("!bg-info/20 text-info", data.IsPreviewMode()), templ.KV("bn-toolbar-muted", !data.IsPreviewMode())} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, togglePreviewMode(data.IsPreviewMode())) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if data.IsBlogPost() && data.ReadingTime > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if data.CanPublish() { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - return nil - }) -} - -// PageInfoDropdown renders the page info and quick actions dropdown -func PageInfoDropdown(data ToolbarData) 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_Var18 := templ.GetChildren(ctx) - if templ_7745c5c3_Var18 == nil { - templ_7745c5c3_Var18 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if data.TodayPageviews > 0 || data.PageviewsTrend != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TodayPageviews)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 515, Col: 91} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " views today
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if data.TrendPercent != 0 { - var templ_7745c5c3_Var20 = []any{"text-xs font-medium", data.TrendColorClass()} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(data.TrendIcon()) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 519, Col: 25} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d%%", abs(data.TrendPercent))) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 519, Col: 73} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - 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, 33, "
") - 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.IsBlogPost() { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if data.AuthorName != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if data.AuthorSlug != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(data.AuthorName) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 535, Col: 100} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(data.AuthorName) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 537, Col: 50} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - if data.ReadingTime > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var27 string - templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d min read", data.ReadingTime)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 542, Col: 86} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if data.WordCount > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d words", data.WordCount)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 546, Col: 84} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
Template ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(data.TemplateName) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 554, Col: 48} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
Modified ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastModifiedFormatted()) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 558, Col: 59} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, copyPageURL(data.PageSlug)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
Move toolbar
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = positionButton(data.PageID, "tl", data.Position, "Top left").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = positionButton(data.PageID, "tc", data.Position, "Top center").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = positionButton(data.PageID, "tr", data.Position, "Top right").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = positionButton(data.PageID, "bl", data.Position, "Bottom left").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = positionButton(data.PageID, "bc", data.Position, "Bottom center").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = positionButton(data.PageID, "br", data.Position, "Bottom right").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if data.HasUnpublishedChanges { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -// positionButton renders a position selector button -func positionButton(pageID uuid.UUID, pos string, currentPos string, label 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_Var37 := templ.GetChildren(ctx) - if templ_7745c5c3_Var37 == nil { - templ_7745c5c3_Var37 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - active := pos == currentPos || (currentPos == "" && pos == "tr") - var templ_7745c5c3_Var38 = []any{"bn-dd-poschip w-8 h-6 rounded border transition-colors flex items-center justify-center", templ.KV("bn-dd-poschip-active", active)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var38...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -// abs returns the absolute value of an integer -func abs(n int) int { - if n < 0 { - return -n - } - return n -} - -// copyPageURL generates the script to copy page URL -func copyPageURL(slug string) templ.ComponentScript { - return templ.ComponentScript{ - Name: `__templ_copyPageURL_044c`, - Function: `function __templ_copyPageURL_044c(slug){const url = window.location.origin + slug; - navigator.clipboard.writeText(url).then(() => { - // Show brief feedback - could enhance with toast later - const btn = event.currentTarget; - const originalText = btn.innerHTML; - btn.innerHTML = ' Copied!'; - setTimeout(() => { btn.innerHTML = originalText; }, 2000); - }); -}`, - Call: templ.SafeScript(`__templ_copyPageURL_044c`, slug), - CallInline: templ.SafeScriptInline(`__templ_copyPageURL_044c`, slug), - } -} - -// togglePreviewMode toggles between preview (draft) and published mode via URL -func togglePreviewMode(isCurrentlyPreview bool) templ.ComponentScript { - return templ.ComponentScript{ - Name: `__templ_togglePreviewMode_114f`, - Function: `function __templ_togglePreviewMode_114f(isCurrentlyPreview){const url = new URL(window.location.href); - if (isCurrentlyPreview) { - // Currently previewing, remove preview param - url.searchParams.delete('preview'); - } else { - // Not previewing, add preview param - url.searchParams.set('preview', '1'); - } - window.location.href = url.toString(); -}`, - Call: templ.SafeScript(`__templ_togglePreviewMode_114f`, isCurrentlyPreview), - CallInline: templ.SafeScriptInline(`__templ_togglePreviewMode_114f`, isCurrentlyPreview), - } -} - -var _ = templruntime.GeneratedTemplate diff --git a/templates/bn/validation.go b/templates/bn/validation.go deleted file mode 100644 index 2315eb5..0000000 --- a/templates/bn/validation.go +++ /dev/null @@ -1,50 +0,0 @@ -package bn - -import ( - "context" - "io" - - "github.com/a-h/templ" -) - -// validationContextKey is the key used to store validation tracking in context. -type validationContextKey struct{} - -// ValidationTracker tracks which bn functions are called during template rendering. -// Used during template registration to validate templates include required calls. -type ValidationTracker struct { - HeadCalled bool - BodyEndCalled bool -} - -// NewValidationContext creates a context with validation tracking enabled. -// Use GetValidationTracker after rendering to check which functions were called. -func NewValidationContext(ctx context.Context) (context.Context, *ValidationTracker) { - tracker := &ValidationTracker{} - return context.WithValue(ctx, validationContextKey{}, tracker), tracker -} - -// GetValidationTracker retrieves the validation tracker from context, if any. -func GetValidationTracker(ctx context.Context) *ValidationTracker { - if tracker, ok := ctx.Value(validationContextKey{}).(*ValidationTracker); ok { - return tracker - } - return nil -} - -// recordValidationCall records that a bn function was called during rendering. -// Returns an empty component (renders nothing) but has the side effect of tracking the call. -func recordValidationCall(ctx context.Context, fnName string) templ.Component { - if tracker := GetValidationTracker(ctx); tracker != nil { - switch fnName { - case "Head": - tracker.HeadCalled = true - case "BodyEnd": - tracker.BodyEndCalled = true - } - } - // Return empty component - renders nothing - return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { - return nil - }) -} diff --git a/templates/pongo/base.html b/templates/pongo/base.html deleted file mode 100644 index 524d4c6..0000000 --- a/templates/pongo/base.html +++ /dev/null @@ -1,9 +0,0 @@ - - -{{ head_html|safe }} - -{{ admin_banner_html|safe }} -{% block body %}{% endblock %} -{{ body_end_html|safe }} - - diff --git a/templates/pongo/context.go b/templates/pongo/context.go deleted file mode 100644 index 460cd5a..0000000 --- a/templates/pongo/context.go +++ /dev/null @@ -1,100 +0,0 @@ -package pongo - -import ( - "context" - "maps" - - "git.dev.alexdunmow.com/block/ninjatpl" - - "git.dev.alexdunmow.com/block/pluginsdk/blocks" - "git.dev.alexdunmow.com/block/pluginsdk/templates/bn" -) - -// buildPageContext builds a ninjatpl.Context with pre-rendered head/body HTML -// and all page-level variables from the standard doc map. -func (e *Engine) buildPageContext(ctx context.Context, doc map[string]any) ninjatpl.Context { - title := "Untitled" - if t, ok := doc["title"].(string); ok && t != "" { - title = t - } - - slots := make(map[string]string) - if s, ok := doc["slots"].(map[string]string); ok { - slots = s - } - - themeMode := "dark" - if tm, ok := doc["theme_mode"].(string); ok && tm != "" { - themeMode = tm - } - - themeCSS := "" - if tc, ok := doc["theme_css"].(string); ok { - themeCSS = tc - } - - structuredData := "" - if sd, ok := doc["structured_data"].(string); ok { - structuredData = sd - } - - cssHash := "" - if ch, ok := doc["css_hash"].(string); ok { - cssHash = ch - } - - pageviewNonce := "" - if pn, ok := doc["pageview_nonce"].(string); ok { - pageviewNonce = pn - } - - settings := bn.ParseSiteSettings(doc) - pageMeta := bn.ParsePageMeta(doc) - engagementConfig := bn.ParseEngagementConfig(doc) - - headHTML := renderComponent(ctx, bn.Head(bn.HeadData{ - Title: title, - Settings: settings, - PageMeta: pageMeta, - ThemeMode: themeMode, - ThemeCSS: themeCSS, - PluginStyles: e.stylePaths, - StructuredData: structuredData, - CSSHash: cssHash, - PageviewNonce: pageviewNonce, - EngagementConfig: engagementConfig, - })) - bodyEndHTML := renderComponent(ctx, bn.BodyEnd(settings)) - bannerHTML := renderComponent(ctx, bn.AdminBypassBanner(settings)) - - slotsAny := make(map[string]any, len(slots)) - for k, v := range slots { - slotsAny[k] = v - } - - return ninjatpl.Context{ - "head_html": headHTML, - "body_end_html": bodyEndHTML, - "admin_banner_html": bannerHTML, - - "title": title, - "slots": slotsAny, - "theme_mode": themeMode, - "theme_css": themeCSS, - "css_hash": cssHash, - - "site_settings": settings, - "page_meta": pageMeta, - } -} - -// buildBlockContext builds a ninjatpl.Context for block rendering. -// Content fields are available directly; request context is under "ctx". -func buildBlockContext(ctx context.Context, content map[string]any) ninjatpl.Context { - pongoCtx := make(ninjatpl.Context, len(content)+1) - maps.Copy(pongoCtx, content) - if bc := blocks.GetBlockContext(ctx); bc != nil { - pongoCtx["ctx"] = bc.ToMap() - } - return pongoCtx -} diff --git a/templates/pongo/embed.go b/templates/pongo/embed.go deleted file mode 100644 index 0067d29..0000000 --- a/templates/pongo/embed.go +++ /dev/null @@ -1,6 +0,0 @@ -package pongo - -import "embed" - -//go:embed base.html -var baseFS embed.FS diff --git a/templates/pongo/engine.go b/templates/pongo/engine.go deleted file mode 100644 index 06b8332..0000000 --- a/templates/pongo/engine.go +++ /dev/null @@ -1,169 +0,0 @@ -package pongo - -import ( - "bytes" - "context" - "io" - "io/fs" - "maps" - - "git.dev.alexdunmow.com/block/ninjatpl" - - "git.dev.alexdunmow.com/block/pluginsdk/blocks" - "git.dev.alexdunmow.com/block/pluginsdk/templates" -) - -// Engine provides first-class pongo2 template support for BlockNinja plugins. -// Plugins create an Engine with their embedded template FS, then call -// MustPageTemplate / MustBlockTemplate to get functions compatible with -// the template and block registries. -type Engine struct { - set *ninjatpl.TemplateSet - stylePaths []string -} - -// NewEngine creates a pongo2 Engine. -// pluginFS should contain the plugin's .html templates. -// stylePaths are CSS URLs included in the page via bn.Head. -func NewEngine(pluginFS fs.FS, stylePaths ...string) *Engine { - loader := &multiLoader{ - loaders: []ninjatpl.TemplateLoader{ - &fsLoader{fsys: pluginFS}, - &fsLoader{fsys: baseFS}, - }, - } - set := ninjatpl.NewSet("plugin", loader) - return &Engine{set: set, stylePaths: stylePaths} -} - -// pongoComponent wraps a pongo2 template execution as an HTMLComponent. -type pongoComponent struct { - tpl *ninjatpl.Template - ctx ninjatpl.Context -} - -func (c *pongoComponent) Render(ctx context.Context, w io.Writer) error { - return c.tpl.ExecuteWriter(c.ctx, w) -} - -// MustPageTemplate parses a pongo2 page template and returns a TemplateFunc. -// Panics on parse error. -// -// Available context variables in page templates: -// -// {{ head_html|safe }} — full element -// {{ body_end_html|safe }} — body-end scripts and admin toolbar -// {{ admin_banner_html|safe }} — maintenance/coming-soon admin banner -// {{ title }} — page title -// {{ slots.header|safe }} — rendered slot HTML -// {{ theme_mode }} — "light", "dark", or "system" -// {{ theme_css }} — raw CSS custom properties -// {{ css_hash }} — cache-busting hash -// {{ site_settings }} — bn.SiteSettingsData struct -// {{ page_meta }} — bn.PageMeta struct -func (e *Engine) MustPageTemplate(name string) templates.TemplateFunc { - tpl := ninjatpl.Must(e.set.FromFile(name)) - return func(ctx context.Context, doc map[string]any) templates.HTMLComponent { - pongoCtx := e.buildPageContext(ctx, doc) - return &pongoComponent{tpl: tpl, ctx: pongoCtx} - } -} - -// MustBlockTemplate parses a pongo2 block template and returns a BlockFunc. -// Panics on parse error. -// -// The content map fields are available directly as template variables. -// Request context is available under {{ ctx.url }}, {{ ctx.isEditor }}, etc. -func (e *Engine) MustBlockTemplate(name string) blocks.BlockFunc { - tpl := ninjatpl.Must(e.set.FromFile(name)) - return func(ctx context.Context, content map[string]any) string { - pongoCtx := buildBlockContext(ctx, content) - out, err := tpl.Execute(pongoCtx) - if err != nil { - return "" - } - return blocks.ProcessIcons(out) - } -} - -// MustBlockTemplateWithDefaults is like MustBlockTemplate but merges default -// values before rendering. Content keys override defaults. -func (e *Engine) MustBlockTemplateWithDefaults(name string, defaults map[string]any) blocks.BlockFunc { - tpl := ninjatpl.Must(e.set.FromFile(name)) - return func(ctx context.Context, content map[string]any) string { - merged := make(map[string]any, len(defaults)+len(content)) - maps.Copy(merged, defaults) - maps.Copy(merged, content) - pongoCtx := buildBlockContext(ctx, merged) - out, err := tpl.Execute(pongoCtx) - if err != nil { - return "" - } - return blocks.ProcessIcons(out) - } -} - -// MustTemplateOverride parses a pongo2 template for use as a block template -// override (e.g., custom heading/text styling). Same as MustBlockTemplate -// but named distinctly for clarity at the call site. -func (e *Engine) MustTemplateOverride(name string) blocks.BlockFunc { - return e.MustBlockTemplate(name) -} - -// MustEmailWrapper parses a pongo2 email template and returns an -// EmailWrapperFunc. Panics on parse error. -// -// Available context variables: -// -// {{ body|safe }} — the email body HTML -// {{ site_name }} — site name -// {{ site_url }} — site URL -// {{ logo_url }} — site logo URL -// {{ unsubscribe_url }} — unsubscribe link (may be empty) -// {{ preview_text }} — email preview/preheader text -// {{ colors.primary }} — primary hex color -// {{ colors.secondary }} — secondary hex color -// {{ colors.background }} — background hex color -// {{ colors.foreground }} — foreground hex color -// {{ colors.border }} — border hex color -// {{ colors.muted }} — muted hex color -// (and all other EmailColors fields as lowercase keys) -func (e *Engine) MustEmailWrapper(name string) templates.EmailWrapperFunc { - tpl := ninjatpl.Must(e.set.FromFile(name)) - return func(body string, ctx templates.EmailContext) string { - pongoCtx := ninjatpl.Context{ - "body": body, - "site_name": ctx.SiteSettings.SiteName, - "site_url": ctx.SiteSettings.SiteURL, - "logo_url": ctx.SiteSettings.LogoURL, - "support_email": ctx.SiteSettings.SupportEmail, - "unsubscribe_url": ctx.UnsubscribeURL, - "preview_text": ctx.PreviewText, - "colors": map[string]any{ - "primary": ctx.Colors.Primary, - "primaryForeground": ctx.Colors.PrimaryForeground, - "secondary": ctx.Colors.Secondary, - "secondaryForeground": ctx.Colors.SecondaryForeground, - "background": ctx.Colors.Background, - "foreground": ctx.Colors.Foreground, - "muted": ctx.Colors.Muted, - "mutedForeground": ctx.Colors.MutedForeground, - "border": ctx.Colors.Border, - "card": ctx.Colors.Card, - "cardForeground": ctx.Colors.CardForeground, - }, - } - out, err := tpl.Execute(pongoCtx) - if err != nil { - return body - } - return out - } -} - -// renderComponent renders any HTMLComponent to a string. -func renderComponent(ctx context.Context, c templates.HTMLComponent) string { - var buf bytes.Buffer - _ = c.Render(ctx, &buf) - return buf.String() -} diff --git a/templates/pongo/loader.go b/templates/pongo/loader.go deleted file mode 100644 index 71b8395..0000000 --- a/templates/pongo/loader.go +++ /dev/null @@ -1,43 +0,0 @@ -package pongo - -import ( - "fmt" - "io" - "io/fs" - - "git.dev.alexdunmow.com/block/ninjatpl" -) - -// fsLoader adapts an fs.FS to pongo2's TemplateLoader interface. -type fsLoader struct { - fsys fs.FS -} - -func (l *fsLoader) Abs(base, name string) string { - return name -} - -func (l *fsLoader) Get(path string) (io.Reader, error) { - return l.fsys.Open(path) -} - -// multiLoader chains multiple loaders, returning the first hit. -// Plugin templates can {% extends "base.html" %} where base.html -// lives in the core embedded FS rather than the plugin FS. -type multiLoader struct { - loaders []ninjatpl.TemplateLoader -} - -func (l *multiLoader) Abs(base, name string) string { - return name -} - -func (l *multiLoader) Get(path string) (io.Reader, error) { - for _, loader := range l.loaders { - r, err := loader.Get(path) - if err == nil { - return r, nil - } - } - return nil, fmt.Errorf("pongo: template %q not found in any loader", path) -}