package components import ( "tangled.org/oolong.social/oolong/internal/profileprefs" "tangled.org/oolong.social/oolong/internal/web/assets" "tangled.org/oolong.social/oolong/internal/web/bff" ) // LayoutData contains all the data needed for the layout type LayoutData struct { Title string IsAuthenticated bool UserDID string UserProfile *bff.UserProfile CSPNonce string IsModerator bool // User has moderation permissions UnreadNotificationCount int // Number of unread notifications UserPreferences profileprefs.UserPreferences // Brand strings, populated from domain.BrandConfig. Empty values fall // back to the oolong defaults via the helper methods below — keeps // existing call sites that build LayoutData directly working until // every handler threads the brand through. BrandName string // "Oolong" BrandTagline string // "Your brew, your data" // AppName is the running app's lowercase identifier (e.g. "oolong", // "oolong"). Used to pick which compiled CSS file to load — empty // falls back to oolong's stylesheet, keeping legacy call sites that // build LayoutData directly working unchanged. AppName string // Assets provides cache-busted CSS and JS hrefs without requiring template // rendering to consult the package-level asset registries. Assets assets.Manifest // OpenGraph metadata (optional, uses defaults if empty) OGTitle string // Falls back to Title + " - " + brand name OGDescription string // Falls back to site description OGImage string // If set, renders og:image tag OGImageAlt string // Alt text for OG image; falls back to OGTitle + OGDescription OGType string // Falls back to "website" OGUrl string // Canonical URL for the page } // stylesheetHref returns the cache-busted CSS URL for the running app. // The bundle is concatenated at server startup from internal/web/assets/css/; // the ?h= query param invalidates automatically when content changes. func (d *LayoutData) stylesheetHref() string { return d.Assets.StylesheetHref(d.AppName) } func (d *LayoutData) scriptHref(name string) string { return d.Assets.ScriptHref(name) } func (d *LayoutData) ScriptHref(name string) string { return d.scriptHref(name) } // brandName returns the configured brand display name, defaulting to // "Oolong" when unset (covers tests and any pre-Phase-G call site that // hasn't been threaded yet). func (d *LayoutData) brandName() string { if d.BrandName != "" { return d.BrandName } return "Oolong" } // brandTagline returns the configured brand tagline, defaulting to the // oolong tagline when unset. func (d *LayoutData) brandTagline() string { if d.BrandTagline != "" { return d.BrandTagline } return "Your brew, your data" } // ogTitle returns the OpenGraph title, falling back to the page title func (d *LayoutData) ogTitle() string { if d.OGTitle != "" { return d.OGTitle } return d.pageTitle() } // pageTitle returns the text, collapsing the home page to just // the brand name. func (d *LayoutData) pageTitle() string { if d.Title == "" || d.Title == "Home" { return d.brandName() } return d.Title + " - " + d.brandName() } // ogDescription returns the OpenGraph description, falling back to site default func (d *LayoutData) ogDescription() string { if d.OGDescription != "" { return d.OGDescription } return d.siteDescription() } func (d *LayoutData) siteDescription() string { if d.AppName == "oolong" { return d.brandName() + " is a tea tracking app built on AT Protocol. Your steep logs, teas, and teaware are stored in your own Personal Data Server, giving you full ownership and portability." } return d.brandName() + " is a tea brew tracking app built on AT Protocol. Your brewing data is stored in your own Personal Data Server, giving you full ownership and portability." } func (d *LayoutData) lightThemeColor() string { if d.AppName == "oolong" { return "#b8d5aa" } return "#4a2c2a" } func (d *LayoutData) darkThemeColor() string { if d.AppName == "oolong" { return "#162018" } return "#0F0A08" } // ogType returns the OpenGraph type, falling back to "website" func (d *LayoutData) ogType() string { if d.OGType != "" { return d.OGType } return "website" } // ogImageAlt returns descriptive alt text for the OG image. func (d *LayoutData) ogImageAlt() string { if d.OGImageAlt != "" { return d.OGImageAlt } alt := d.ogTitle() if d.OGDescription != "" { alt += " — " + d.OGDescription } return alt } // twitterCardType returns "summary_large_image" when an OG image is set, // otherwise "summary" for the default compact card. func (d *LayoutData) twitterCardType() string { if d.OGImage != "" { return "summary_large_image" } return "summary" } templ Layout(data *LayoutData, content templ.Component) { <!DOCTYPE html> <html lang="en" class="h-full" style="background-color: var(--page-bg);"> <head> <script nonce={ data.CSPNonce }> // Apply theme before render to prevent flash (function() { var t = localStorage.getItem('oolong-theme'); if (t === 'dark' || t === 'light') document.documentElement.setAttribute('data-theme', t); })(); </script> <meta charset="UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="description" content={ data.siteDescription() }/> <!-- OpenGraph metadata --> <meta property="og:title" content={ data.ogTitle() }/> <meta property="og:description" content={ data.ogDescription() }/> <meta property="og:type" content={ data.ogType() }/> <meta property="og:site_name" content={ data.brandName() }/> if data.OGUrl != "" { <meta property="og:url" content={ data.OGUrl }/> } if data.OGImage != "" { <meta property="og:image" content={ data.OGImage }/> <meta property="og:image:width" content="1200"/> <meta property="og:image:height" content="630"/> <meta property="og:image:alt" content={ data.ogImageAlt() }/> } <!-- Twitter Card metadata --> <meta name="twitter:card" content={ data.twitterCardType() }/> <meta name="twitter:title" content={ data.ogTitle() }/> <meta name="twitter:description" content={ data.ogDescription() }/> if data.OGImage != "" { <meta name="twitter:image" content={ data.OGImage }/> } <meta name="theme-color" content={ data.lightThemeColor() } media="(prefers-color-scheme: light)"/> <meta name="theme-color" content={ data.darkThemeColor() } media="(prefers-color-scheme: dark)"/> <title>{ data.pageTitle() } if tp := bff.Traceparent(ctx); tp != "" { } @HeaderWithProps(HeaderProps{ IsAuthenticated: data.IsAuthenticated, UserProfile: data.UserProfile, UserDID: data.UserDID, IsModerator: data.IsModerator, UnreadNotificationCount: data.UnreadNotificationCount, BrandName: data.brandName(), AppName: data.AppName, })
@content
@FooterWithBrand(data.brandName(), data.brandTagline())
}