Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m51s
Build and Release / Lint (push) Failing after 8m33s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 8m46s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Add configurable comparison matrix section for feature comparisons. Supports 3 columns (e.g., your product vs competitors, or pricing tiers) with optional highlight column. Features can be organized into groups. Each cell accepts "true" for checkmark, "false" for X, or custom text. Includes settings UI with up to 10 groups and 20 features per group. Renders in all 9 page templates with responsive table styling.
461 lines
15 KiB
Go
461 lines
15 KiB
Go
// 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"`
|
|
PublicLanding bool `yaml:"public_landing"`
|
|
Template string `yaml:"template"` // open-source-hero, minimalist-docs, saas-conversion, bold-marketing
|
|
|
|
// Custom domain (optional)
|
|
Domain string `yaml:"domain,omitempty"`
|
|
|
|
// Brand configuration
|
|
Brand BrandConfig `yaml:"brand,omitempty"`
|
|
|
|
// Hero section
|
|
Hero HeroConfig `yaml:"hero,omitempty"`
|
|
|
|
// Stats/metrics
|
|
Stats []StatConfig `yaml:"stats,omitempty"`
|
|
|
|
// Value propositions
|
|
ValueProps []ValuePropConfig `yaml:"value_props,omitempty"`
|
|
|
|
// Features
|
|
Features []FeatureConfig `yaml:"features,omitempty"`
|
|
|
|
// Social proof
|
|
SocialProof SocialProofConfig `yaml:"social_proof,omitempty"`
|
|
|
|
// Pricing (for saas-conversion template)
|
|
Pricing PricingConfig `yaml:"pricing,omitempty"`
|
|
|
|
// CTA section
|
|
CTASection CTASectionConfig `yaml:"cta_section,omitempty"`
|
|
|
|
// Blog section
|
|
Blog BlogSectionConfig `yaml:"blog,omitempty"`
|
|
|
|
// Gallery section
|
|
Gallery GallerySectionConfig `yaml:"gallery,omitempty"`
|
|
|
|
// Comparison section
|
|
Comparison ComparisonSectionConfig `yaml:"comparison,omitempty"`
|
|
|
|
// Navigation visibility
|
|
Navigation NavigationConfig `yaml:"navigation,omitempty"`
|
|
|
|
// Footer
|
|
Footer FooterConfig `yaml:"footer,omitempty"`
|
|
|
|
// Theme customization
|
|
Theme ThemeConfig `yaml:"theme,omitempty"`
|
|
|
|
// SEO & Social
|
|
SEO SEOConfig `yaml:"seo,omitempty"`
|
|
|
|
// Analytics
|
|
Analytics AnalyticsConfig `yaml:"analytics,omitempty"`
|
|
|
|
// Advanced settings
|
|
Advanced AdvancedConfig `yaml:"advanced,omitempty"`
|
|
|
|
// A/B testing experiments
|
|
Experiments ExperimentConfig `yaml:"experiments,omitempty"`
|
|
|
|
// Multi-language support
|
|
I18n I18nConfig `yaml:"i18n,omitempty"`
|
|
}
|
|
|
|
// BrandConfig represents brand/identity settings
|
|
type BrandConfig struct {
|
|
Name string `yaml:"name,omitempty"`
|
|
LogoURL string `yaml:"logo_url,omitempty"`
|
|
UploadedLogo string `yaml:"uploaded_logo,omitempty"`
|
|
LogoSource string `yaml:"logo_source,omitempty"` // "url" (default), "repo", or "org" — selects avatar source
|
|
Tagline string `yaml:"tagline,omitempty"`
|
|
FaviconURL string `yaml:"favicon_url,omitempty"`
|
|
UploadedFavicon string `yaml:"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"`
|
|
Subheadline string `yaml:"subheadline,omitempty"`
|
|
PrimaryCTA CTAButton `yaml:"primary_cta,omitempty"`
|
|
SecondaryCTA CTAButton `yaml:"secondary_cta,omitempty"`
|
|
ImageURL string `yaml:"image_url,omitempty"`
|
|
UploadedImage string `yaml:"uploaded_image,omitempty"` // filename in repo-avatars storage
|
|
CodeExample string `yaml:"code_example,omitempty"`
|
|
VideoURL string `yaml:"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"`
|
|
URL string `yaml:"url,omitempty"`
|
|
Variant string `yaml:"variant,omitempty"` // primary, secondary, outline, text
|
|
}
|
|
|
|
// StatConfig represents a single stat/metric
|
|
type StatConfig struct {
|
|
Value string `yaml:"value,omitempty"`
|
|
Label string `yaml:"label,omitempty"`
|
|
}
|
|
|
|
// ValuePropConfig represents a value proposition
|
|
type ValuePropConfig struct {
|
|
Title string `yaml:"title,omitempty"`
|
|
Description string `yaml:"description,omitempty"`
|
|
Icon string `yaml:"icon,omitempty"`
|
|
}
|
|
|
|
// FeatureConfig represents a single feature item
|
|
type FeatureConfig struct {
|
|
Title string `yaml:"title,omitempty"`
|
|
Description string `yaml:"description,omitempty"`
|
|
Icon string `yaml:"icon,omitempty"`
|
|
ImageURL string `yaml:"image_url,omitempty"`
|
|
}
|
|
|
|
// SocialProofConfig represents social proof section
|
|
type SocialProofConfig struct {
|
|
Logos []string `yaml:"logos,omitempty"`
|
|
Testimonial TestimonialConfig `yaml:"testimonial,omitempty"`
|
|
Testimonials []TestimonialConfig `yaml:"testimonials,omitempty"`
|
|
}
|
|
|
|
// TestimonialConfig represents a testimonial
|
|
type TestimonialConfig struct {
|
|
Quote string `yaml:"quote,omitempty"`
|
|
Author string `yaml:"author,omitempty"`
|
|
Role string `yaml:"role,omitempty"`
|
|
Avatar string `yaml:"avatar,omitempty"`
|
|
}
|
|
|
|
// PricingConfig represents pricing section
|
|
type PricingConfig struct {
|
|
Headline string `yaml:"headline,omitempty"`
|
|
Subheadline string `yaml:"subheadline,omitempty"`
|
|
Plans []PricingPlanConfig `yaml:"plans,omitempty"`
|
|
}
|
|
|
|
// PricingPlanConfig represents a pricing plan
|
|
type PricingPlanConfig struct {
|
|
Name string `yaml:"name,omitempty"`
|
|
Price string `yaml:"price,omitempty"`
|
|
Period string `yaml:"period,omitempty"`
|
|
Features []string `yaml:"features,omitempty"`
|
|
CTA string `yaml:"cta,omitempty"`
|
|
Featured bool `yaml:"featured,omitempty"`
|
|
}
|
|
|
|
// CTASectionConfig represents the final CTA section
|
|
type CTASectionConfig struct {
|
|
Headline string `yaml:"headline,omitempty"`
|
|
Subheadline string `yaml:"subheadline,omitempty"`
|
|
Button CTAButton `yaml:"button,omitempty"`
|
|
}
|
|
|
|
// BlogSectionConfig represents blog section settings on the landing page
|
|
type BlogSectionConfig struct {
|
|
Enabled bool `yaml:"enabled,omitempty"`
|
|
Headline string `yaml:"headline,omitempty"`
|
|
Subheadline string `yaml:"subheadline,omitempty"`
|
|
MaxPosts int `yaml:"max_posts,omitempty"` // default 3
|
|
ShowExcerpt bool `yaml:"show_excerpt,omitempty"` // show subtitle as excerpt
|
|
CTAButton CTAButton `yaml:"cta_button,omitempty"` // "View All Posts" link
|
|
}
|
|
|
|
// GallerySectionConfig represents gallery section settings on the landing page
|
|
type GallerySectionConfig struct {
|
|
Enabled bool `yaml:"enabled,omitempty"`
|
|
Headline string `yaml:"headline,omitempty"`
|
|
Subheadline string `yaml:"subheadline,omitempty"`
|
|
MaxImages int `yaml:"max_images,omitempty"` // default 6
|
|
Columns int `yaml:"columns,omitempty"` // grid columns, default 3
|
|
}
|
|
|
|
// ComparisonSectionConfig represents a feature comparison matrix section
|
|
type ComparisonSectionConfig struct {
|
|
Enabled bool `yaml:"enabled,omitempty"`
|
|
Headline string `yaml:"headline,omitempty"`
|
|
Subheadline string `yaml:"subheadline,omitempty"`
|
|
Columns []ComparisonColumnConfig `yaml:"columns,omitempty"`
|
|
Groups []ComparisonGroupConfig `yaml:"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"`
|
|
Highlight bool `yaml:"highlight,omitempty"`
|
|
}
|
|
|
|
// ComparisonGroupConfig represents a group of features in the comparison table
|
|
type ComparisonGroupConfig struct {
|
|
Name string `yaml:"name,omitempty"`
|
|
Features []ComparisonFeatureConfig `yaml:"features,omitempty"`
|
|
}
|
|
|
|
// ComparisonFeatureConfig represents a single feature row in the comparison table
|
|
type ComparisonFeatureConfig struct {
|
|
Name string `yaml:"name,omitempty"`
|
|
Values []string `yaml:"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"`
|
|
ShowAPI bool `yaml:"show_api,omitempty"`
|
|
ShowRepository bool `yaml:"show_repository,omitempty"`
|
|
ShowReleases bool `yaml:"show_releases,omitempty"`
|
|
ShowIssues bool `yaml:"show_issues,omitempty"`
|
|
}
|
|
|
|
// FooterConfig represents footer settings
|
|
type FooterConfig struct {
|
|
Links []FooterLink `yaml:"links,omitempty"`
|
|
Social []SocialLink `yaml:"social,omitempty"`
|
|
Copyright string `yaml:"copyright,omitempty"`
|
|
ShowPoweredBy bool `yaml:"show_powered_by,omitempty"`
|
|
}
|
|
|
|
// FooterLink represents a single footer link
|
|
type FooterLink struct {
|
|
Label string `yaml:"label,omitempty"`
|
|
URL string `yaml:"url,omitempty"`
|
|
}
|
|
|
|
// SocialLink represents a social media link
|
|
type SocialLink struct {
|
|
Platform string `yaml:"platform,omitempty"` // bluesky, discord, facebook, github, instagram, linkedin, mastodon, reddit, rss, substack, threads, tiktok, twitch, twitter, youtube
|
|
URL string `yaml:"url,omitempty"`
|
|
}
|
|
|
|
// ThemeConfig represents theme customization
|
|
type ThemeConfig struct {
|
|
PrimaryColor string `yaml:"primary_color,omitempty"`
|
|
AccentColor string `yaml:"accent_color,omitempty"`
|
|
Mode string `yaml:"mode,omitempty"` // light, dark, auto
|
|
}
|
|
|
|
// SEOConfig represents SEO and social sharing settings
|
|
type SEOConfig struct {
|
|
Title string `yaml:"title,omitempty"`
|
|
Description string `yaml:"description,omitempty"`
|
|
Keywords []string `yaml:"keywords,omitempty"`
|
|
OGImage string `yaml:"og_image,omitempty"`
|
|
UseMediaKitOG bool `yaml:"use_media_kit_og,omitempty"`
|
|
TwitterCard string `yaml:"twitter_card,omitempty"`
|
|
TwitterSite string `yaml:"twitter_site,omitempty"`
|
|
}
|
|
|
|
// AnalyticsConfig represents analytics settings
|
|
type AnalyticsConfig struct {
|
|
Plausible string `yaml:"plausible,omitempty"`
|
|
Umami UmamiConfig `yaml:"umami,omitempty"`
|
|
GoogleAnalytics string `yaml:"google_analytics,omitempty"`
|
|
}
|
|
|
|
// UmamiConfig represents Umami analytics settings
|
|
type UmamiConfig struct {
|
|
WebsiteID string `yaml:"website_id,omitempty"`
|
|
URL string `yaml:"url,omitempty"`
|
|
}
|
|
|
|
// ExperimentConfig represents A/B testing experiment settings
|
|
type ExperimentConfig struct {
|
|
Enabled bool `yaml:"enabled,omitempty"`
|
|
AutoOptimize bool `yaml:"auto_optimize,omitempty"`
|
|
MinImpressions int `yaml:"min_impressions,omitempty"`
|
|
ApprovalRequired bool `yaml:"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 display names
|
|
func LanguageDisplayNames() map[string]string {
|
|
return map[string]string{
|
|
"en": "English",
|
|
"es": "Espanol",
|
|
"de": "Deutsch",
|
|
"fr": "Francais",
|
|
"ja": "Japanese",
|
|
"zh": "Chinese",
|
|
"pt": "Portugues",
|
|
"ru": "Russian",
|
|
"ko": "Korean",
|
|
"it": "Italiano",
|
|
"hi": "Hindi",
|
|
"ar": "Arabic",
|
|
"nl": "Nederlands",
|
|
"pl": "Polski",
|
|
"tr": "Turkish",
|
|
}
|
|
}
|
|
|
|
// AdvancedConfig represents advanced settings
|
|
type AdvancedConfig struct {
|
|
CustomCSS string `yaml:"custom_css,omitempty"`
|
|
CustomHead string `yaml:"custom_head,omitempty"`
|
|
Redirects map[string]string `yaml:"redirects,omitempty"`
|
|
PublicReleases bool `yaml:"public_releases,omitempty"`
|
|
HideMobileReleases bool `yaml:"hide_mobile_releases,omitempty"`
|
|
GooglePlayID string `yaml:"google_play_id,omitempty"`
|
|
AppStoreID string `yaml:"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",
|
|
}
|
|
}
|