// Copyright 2026 MarketAlly. All rights reserved. // SPDX-License-Identifier: MIT package pages import ( "crypto/sha256" "encoding/hex" "slices" "gopkg.in/yaml.v3" ) // LandingConfig represents the parsed .gitea/landing.yaml configuration type LandingConfig struct { Enabled bool `yaml:"enabled" json:"enabled"` PublicLanding bool `yaml:"public_landing" json:"public_landing"` Template string `yaml:"template" json:"template"` // open-source-hero, minimalist-docs, saas-conversion, bold-marketing // Custom domain (optional) Domain string `yaml:"domain,omitempty" json:"domain,omitempty"` // Brand configuration Brand BrandConfig `yaml:"brand,omitempty" json:"brand,omitzero"` // Hero section Hero HeroConfig `yaml:"hero,omitempty" json:"hero,omitzero"` // Stats/metrics Stats []StatConfig `yaml:"stats,omitempty" json:"stats,omitempty"` // Value propositions ValueProps []ValuePropConfig `yaml:"value_props,omitempty" json:"value_props,omitempty"` // Features Features []FeatureConfig `yaml:"features,omitempty" json:"features,omitempty"` // Social proof SocialProof SocialProofConfig `yaml:"social_proof,omitempty" json:"social_proof,omitzero"` // Pricing (for saas-conversion template) Pricing PricingConfig `yaml:"pricing,omitempty" json:"pricing,omitzero"` // CTA section CTASection CTASectionConfig `yaml:"cta_section,omitempty" json:"cta_section,omitzero"` // Blog section Blog BlogSectionConfig `yaml:"blog,omitempty" json:"blog,omitzero"` // Gallery section Gallery GallerySectionConfig `yaml:"gallery,omitempty" json:"gallery,omitzero"` // Comparison section Comparison ComparisonSectionConfig `yaml:"comparison,omitempty" json:"comparison,omitzero"` // Navigation visibility Navigation NavigationConfig `yaml:"navigation,omitempty" json:"navigation,omitzero"` // Footer Footer FooterConfig `yaml:"footer,omitempty" json:"footer,omitzero"` // Theme customization Theme ThemeConfig `yaml:"theme,omitempty" json:"theme,omitzero"` // SEO & Social SEO SEOConfig `yaml:"seo,omitempty" json:"seo,omitzero"` // Analytics Analytics AnalyticsConfig `yaml:"analytics,omitempty" json:"analytics,omitzero"` // Advanced settings Advanced AdvancedConfig `yaml:"advanced,omitempty" json:"advanced,omitzero"` // A/B testing experiments Experiments ExperimentConfig `yaml:"experiments,omitempty" json:"experiments,omitzero"` // Multi-language support I18n I18nConfig `yaml:"i18n,omitempty" json:"i18n,omitzero"` } // BrandConfig represents brand/identity settings type BrandConfig struct { Name string `yaml:"name,omitempty" json:"name,omitempty"` LogoURL string `yaml:"logo_url,omitempty" json:"logo_url,omitempty"` UploadedLogo string `yaml:"uploaded_logo,omitempty" json:"uploaded_logo,omitempty"` LogoSource string `yaml:"logo_source,omitempty" json:"logo_source,omitempty"` // "url" (default), "repo", or "org" — selects avatar source Tagline string `yaml:"tagline,omitempty" json:"tagline,omitempty"` FaviconURL string `yaml:"favicon_url,omitempty" json:"favicon_url,omitempty"` UploadedFavicon string `yaml:"uploaded_favicon,omitempty" json:"uploaded_favicon,omitempty"` } // ResolvedLogoURL returns the uploaded logo path or the external URL. func (b *BrandConfig) ResolvedLogoURL() string { if b.UploadedLogo != "" { return "/repo-avatars/" + b.UploadedLogo } return b.LogoURL } // ResolvedFaviconURL returns the uploaded favicon path or the external URL. func (b *BrandConfig) ResolvedFaviconURL() string { if b.UploadedFavicon != "" { return "/repo-avatars/" + b.UploadedFavicon } return b.FaviconURL } // HeroConfig represents hero section settings type HeroConfig struct { Headline string `yaml:"headline,omitempty" json:"headline,omitempty"` Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"` PrimaryCTA CTAButton `yaml:"primary_cta,omitempty" json:"primary_cta,omitzero"` SecondaryCTA CTAButton `yaml:"secondary_cta,omitempty" json:"secondary_cta,omitzero"` ImageURL string `yaml:"image_url,omitempty" json:"image_url,omitempty"` UploadedImage string `yaml:"uploaded_image,omitempty" json:"uploaded_image,omitempty"` // filename in repo-avatars storage CodeExample string `yaml:"code_example,omitempty" json:"code_example,omitempty"` VideoURL string `yaml:"video_url,omitempty" json:"video_url,omitempty"` } // ResolvedImageURL returns the effective hero image URL, preferring uploaded image over URL. func (h *HeroConfig) ResolvedImageURL() string { if h.UploadedImage != "" { return "/repo-avatars/" + h.UploadedImage } return h.ImageURL } // CTAButton represents a call-to-action button type CTAButton struct { Label string `yaml:"label,omitempty" json:"label,omitempty"` URL string `yaml:"url,omitempty" json:"url,omitempty"` Variant string `yaml:"variant,omitempty" json:"variant,omitempty"` // primary, secondary, outline, text } // StatConfig represents a single stat/metric type StatConfig struct { Value string `yaml:"value,omitempty" json:"value,omitempty"` Label string `yaml:"label,omitempty" json:"label,omitempty"` } // ValuePropConfig represents a value proposition type ValuePropConfig struct { Title string `yaml:"title,omitempty" json:"title,omitempty"` Description string `yaml:"description,omitempty" json:"description,omitempty"` Icon string `yaml:"icon,omitempty" json:"icon,omitempty"` } // FeatureConfig represents a single feature item type FeatureConfig struct { Title string `yaml:"title,omitempty" json:"title,omitempty"` Description string `yaml:"description,omitempty" json:"description,omitempty"` Icon string `yaml:"icon,omitempty" json:"icon,omitempty"` ImageURL string `yaml:"image_url,omitempty" json:"image_url,omitempty"` } // SocialProofConfig represents social proof section type SocialProofConfig struct { Logos []string `yaml:"logos,omitempty" json:"logos,omitempty"` Testimonial TestimonialConfig `yaml:"testimonial,omitempty" json:"testimonial,omitzero"` Testimonials []TestimonialConfig `yaml:"testimonials,omitempty" json:"testimonials,omitempty"` } // TestimonialConfig represents a testimonial type TestimonialConfig struct { Quote string `yaml:"quote,omitempty" json:"quote,omitempty"` Author string `yaml:"author,omitempty" json:"author,omitempty"` Role string `yaml:"role,omitempty" json:"role,omitempty"` Avatar string `yaml:"avatar,omitempty" json:"avatar,omitempty"` } // PricingConfig represents pricing section type PricingConfig struct { Headline string `yaml:"headline,omitempty" json:"headline,omitempty"` Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"` Plans []PricingPlanConfig `yaml:"plans,omitempty" json:"plans,omitempty"` } // PricingPlanConfig represents a pricing plan type PricingPlanConfig struct { Name string `yaml:"name,omitempty" json:"name,omitempty"` Price string `yaml:"price,omitempty" json:"price,omitempty"` Period string `yaml:"period,omitempty" json:"period,omitempty"` Features []string `yaml:"features,omitempty" json:"features,omitempty"` CTA string `yaml:"cta,omitempty" json:"cta,omitempty"` Featured bool `yaml:"featured,omitempty" json:"featured,omitempty"` } // CTASectionConfig represents the final CTA section type CTASectionConfig struct { Headline string `yaml:"headline,omitempty" json:"headline,omitempty"` Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"` Button CTAButton `yaml:"button,omitempty" json:"button,omitzero"` } // BlogSectionConfig represents blog section settings on the landing page type BlogSectionConfig struct { Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` Headline string `yaml:"headline,omitempty" json:"headline,omitempty"` Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"` MaxPosts int `yaml:"max_posts,omitempty" json:"max_posts,omitempty"` // default 3 ShowExcerpt bool `yaml:"show_excerpt,omitempty" json:"show_excerpt,omitempty"` // show subtitle as excerpt CTAButton CTAButton `yaml:"cta_button,omitempty" json:"cta_button,omitzero"` // "View All Posts" link } // GallerySectionConfig represents gallery section settings on the landing page type GallerySectionConfig struct { Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` Headline string `yaml:"headline,omitempty" json:"headline,omitempty"` Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"` MaxImages int `yaml:"max_images,omitempty" json:"max_images,omitempty"` // default 6 Columns int `yaml:"columns,omitempty" json:"columns,omitempty"` // grid columns, default 3 } // ComparisonSectionConfig represents a feature comparison matrix section type ComparisonSectionConfig struct { Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` Headline string `yaml:"headline,omitempty" json:"headline,omitempty"` Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"` Columns []ComparisonColumnConfig `yaml:"columns,omitempty" json:"columns,omitempty"` Groups []ComparisonGroupConfig `yaml:"groups,omitempty" json:"groups,omitempty"` } // HasData returns true if the comparison section has columns and at least one feature func (c *ComparisonSectionConfig) HasData() bool { if len(c.Columns) == 0 || len(c.Groups) == 0 { return false } for _, g := range c.Groups { if len(g.Features) > 0 { return true } } return false } // ComparisonColumnConfig represents a column header in the comparison table type ComparisonColumnConfig struct { Name string `yaml:"name,omitempty" json:"name,omitempty"` Highlight bool `yaml:"highlight,omitempty" json:"highlight,omitempty"` } // ComparisonGroupConfig represents a group of features in the comparison table type ComparisonGroupConfig struct { Name string `yaml:"name,omitempty" json:"name,omitempty"` Features []ComparisonFeatureConfig `yaml:"features,omitempty" json:"features,omitempty"` } // ComparisonFeatureConfig represents a single feature row in the comparison table type ComparisonFeatureConfig struct { Name string `yaml:"name,omitempty" json:"name,omitempty"` Values []string `yaml:"values,omitempty" json:"values,omitempty"` // "true"/"false" for check/x, anything else displayed as text } // NavigationConfig controls which built-in navigation links appear in the header and footer type NavigationConfig struct { ShowDocs bool `yaml:"show_docs,omitempty" json:"show_docs,omitempty"` ShowAPI bool `yaml:"show_api,omitempty" json:"show_api,omitempty"` ShowRepository bool `yaml:"show_repository,omitempty" json:"show_repository,omitempty"` ShowReleases bool `yaml:"show_releases,omitempty" json:"show_releases,omitempty"` ShowIssues bool `yaml:"show_issues,omitempty" json:"show_issues,omitempty"` // Translatable labels for nav items and section headers (defaults to English) LabelValueProps string `yaml:"label_value_props,omitempty" json:"label_value_props,omitempty"` LabelFeatures string `yaml:"label_features,omitempty" json:"label_features,omitempty"` LabelPricing string `yaml:"label_pricing,omitempty" json:"label_pricing,omitempty"` LabelBlog string `yaml:"label_blog,omitempty" json:"label_blog,omitempty"` LabelGallery string `yaml:"label_gallery,omitempty" json:"label_gallery,omitempty"` LabelCompare string `yaml:"label_compare,omitempty" json:"label_compare,omitempty"` LabelDocs string `yaml:"label_docs,omitempty" json:"label_docs,omitempty"` LabelReleases string `yaml:"label_releases,omitempty" json:"label_releases,omitempty"` LabelAPI string `yaml:"label_api,omitempty" json:"label_api,omitempty"` LabelIssues string `yaml:"label_issues,omitempty" json:"label_issues,omitempty"` } // FooterConfig represents footer settings type FooterConfig struct { Links []FooterLink `yaml:"links,omitempty" json:"links,omitempty"` Social []SocialLink `yaml:"social,omitempty" json:"social,omitempty"` Copyright string `yaml:"copyright,omitempty" json:"copyright,omitempty"` ShowPoweredBy bool `yaml:"show_powered_by,omitempty" json:"show_powered_by,omitempty"` } // FooterLink represents a single footer link type FooterLink struct { Label string `yaml:"label,omitempty" json:"label,omitempty"` URL string `yaml:"url,omitempty" json:"url,omitempty"` } // SocialLink represents a social media link type SocialLink struct { Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` // bluesky, discord, facebook, github, instagram, linkedin, mastodon, reddit, rss, substack, threads, tiktok, twitch, twitter, youtube URL string `yaml:"url,omitempty" json:"url,omitempty"` } // ThemeConfig represents theme customization type ThemeConfig struct { PrimaryColor string `yaml:"primary_color,omitempty" json:"primary_color,omitempty"` AccentColor string `yaml:"accent_color,omitempty" json:"accent_color,omitempty"` Mode string `yaml:"mode,omitempty" json:"mode,omitempty"` // light, dark, auto } // SEOConfig represents SEO and social sharing settings type SEOConfig struct { Title string `yaml:"title,omitempty" json:"title,omitempty"` Description string `yaml:"description,omitempty" json:"description,omitempty"` Keywords []string `yaml:"keywords,omitempty" json:"keywords,omitempty"` OGImage string `yaml:"og_image,omitempty" json:"og_image,omitempty"` UseMediaKitOG bool `yaml:"use_media_kit_og,omitempty" json:"use_media_kit_og,omitempty"` TwitterCard string `yaml:"twitter_card,omitempty" json:"twitter_card,omitempty"` TwitterSite string `yaml:"twitter_site,omitempty" json:"twitter_site,omitempty"` } // AnalyticsConfig represents analytics settings type AnalyticsConfig struct { Plausible string `yaml:"plausible,omitempty" json:"plausible,omitempty"` Umami UmamiConfig `yaml:"umami,omitempty" json:"umami,omitzero"` GoogleAnalytics string `yaml:"google_analytics,omitempty" json:"google_analytics,omitempty"` } // UmamiConfig represents Umami analytics settings type UmamiConfig struct { WebsiteID string `yaml:"website_id,omitempty" json:"website_id,omitempty"` URL string `yaml:"url,omitempty" json:"url,omitempty"` } // ExperimentConfig represents A/B testing experiment settings type ExperimentConfig struct { Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` AutoOptimize bool `yaml:"auto_optimize,omitempty" json:"auto_optimize,omitempty"` MinImpressions int `yaml:"min_impressions,omitempty" json:"min_impressions,omitempty"` ApprovalRequired bool `yaml:"approval_required,omitempty" json:"approval_required,omitempty"` } // I18nConfig represents multi-language settings for the landing page type I18nConfig struct { DefaultLang string `yaml:"default_lang,omitempty" json:"default_lang,omitempty"` Languages []string `yaml:"languages,omitempty" json:"languages,omitempty"` } // LanguageDisplayNames returns a map of language codes to native display names func LanguageDisplayNames() map[string]string { return map[string]string{ "en": "English", "es": "Español", "de": "Deutsch", "fr": "Français", "ja": "日本語", "zh": "中文", "pt": "Português", "ru": "Русский", "ko": "한국어", "it": "Italiano", "hi": "हिन्दी", "ar": "العربية", "nl": "Nederlands", "pl": "Polski", "tr": "Türkçe", } } // AdvancedConfig represents advanced settings type AdvancedConfig struct { CustomCSS string `yaml:"custom_css,omitempty" json:"custom_css,omitempty"` CustomHead string `yaml:"custom_head,omitempty" json:"custom_head,omitempty"` Redirects map[string]string `yaml:"redirects,omitempty" json:"redirects,omitempty"` StaticRoutes []string `yaml:"static_routes,omitempty" json:"static_routes,omitempty"` PublicReleases bool `yaml:"public_releases,omitempty" json:"public_releases,omitempty"` HideMobileReleases bool `yaml:"hide_mobile_releases,omitempty" json:"hide_mobile_releases,omitempty"` GooglePlayID string `yaml:"google_play_id,omitempty" json:"google_play_id,omitempty"` AppStoreID string `yaml:"app_store_id,omitempty" json:"app_store_id,omitempty"` } // ParseLandingConfig parses a landing.yaml file content func ParseLandingConfig(content []byte) (*LandingConfig, error) { config := &LandingConfig{ Enabled: true, Template: "open-source-hero", } if err := yaml.Unmarshal(content, config); err != nil { return nil, err } // Apply defaults if config.Template == "" { config.Template = "open-source-hero" } if config.Theme.Mode == "" { config.Theme.Mode = "auto" } return config, nil } // HashConfig returns a SHA256 hash of the config content for change detection func HashConfig(content []byte) string { hash := sha256.Sum256(content) return hex.EncodeToString(hash[:]) } // DefaultConfig returns a default landing page configuration func DefaultConfig() *LandingConfig { return &LandingConfig{ Enabled: true, Template: "open-source-hero", Hero: HeroConfig{ Headline: "Build something amazing", Subheadline: "A powerful toolkit for developers who want to ship fast.", PrimaryCTA: CTAButton{ Label: "Get Started", URL: "#", }, SecondaryCTA: CTAButton{ Label: "View on GitHub", URL: "#", }, }, Stats: []StatConfig{ {Value: "10k+", Label: "Downloads"}, {Value: "100+", Label: "Contributors"}, {Value: "MIT", Label: "License"}, }, ValueProps: []ValuePropConfig{ {Title: "Fast", Description: "Optimized for performance out of the box.", Icon: "zap"}, {Title: "Flexible", Description: "Adapts to your workflow, not the other way around.", Icon: "gear"}, {Title: "Open Source", Description: "Free forever. Community driven.", Icon: "heart"}, }, Navigation: NavigationConfig{ ShowDocs: true, ShowRepository: true, ShowReleases: true, }, CTASection: CTASectionConfig{ Headline: "Ready to get started?", Subheadline: "Join thousands of developers already using this project.", Button: CTAButton{ Label: "Get Started Free", URL: "#", }, }, Footer: FooterConfig{ ShowPoweredBy: true, }, Theme: ThemeConfig{ Mode: "auto", }, } } // ValidTemplates returns the list of valid template names func ValidTemplates() []string { return []string{"open-source-hero", "minimalist-docs", "saas-conversion", "bold-marketing", "documentation-first", "developer-tool", "visual-showcase", "cli-terminal", "architecture-deep-dive"} } // IsValidTemplate checks if a template name is valid func IsValidTemplate(name string) bool { return slices.Contains(ValidTemplates(), name) } // TemplateDisplayNames returns a map of template names to display names func TemplateDisplayNames() map[string]string { return map[string]string{ "open-source-hero": "Open Source Product", "minimalist-docs": "Minimalist Product", "saas-conversion": "SaaS Product", "bold-marketing": "Bold Marketing Product", "documentation-first": "Documentation First", "developer-tool": "Developer Tool", "visual-showcase": "Visual Showcase", "cli-terminal": "CLI Terminal", "architecture-deep-dive": "Architecture Deep Dive", } } // TemplateDefaultLabels returns the template-specific default section labels. // These are the creative names each template uses for its sections. func TemplateDefaultLabels(template string) NavigationConfig { switch template { case "architecture-deep-dive": return NavigationConfig{ LabelValueProps: "Systems Analysis", LabelFeatures: "Technical Specifications", LabelPricing: "Resource Allocation", LabelBlog: "Dispatches", LabelGallery: "Visual Index", LabelCompare: "Compare", } case "bold-marketing": return NavigationConfig{ LabelValueProps: "Why choose this", LabelFeatures: "Capabilities", LabelPricing: "Investment", LabelBlog: "Blog", LabelGallery: "Gallery", LabelCompare: "Compare", } case "minimalist-docs": return NavigationConfig{ LabelValueProps: "Why choose this", LabelFeatures: "Capabilities", LabelPricing: "Investment", LabelBlog: "Blog", LabelGallery: "Gallery", LabelCompare: "Compare", } case "open-source-hero": return NavigationConfig{ LabelValueProps: "Why choose us", LabelFeatures: "Capabilities", LabelPricing: "Pricing", LabelBlog: "Blog", LabelGallery: "Gallery", LabelCompare: "Compare", } case "saas-conversion": return NavigationConfig{ LabelValueProps: "Why", LabelFeatures: "Features", LabelPricing: "Pricing", LabelBlog: "Blog", LabelGallery: "Gallery", LabelCompare: "Compare", } default: // developer-tool, documentation-first, visual-showcase, cli-terminal return NavigationConfig{ LabelValueProps: "Why choose us", LabelFeatures: "Capabilities", LabelPricing: "Pricing", LabelBlog: "Blog", LabelGallery: "Gallery", LabelCompare: "Compare", } } }