2
0
Files
gitcaddy-server/modules/pages/config.go
logikonline 00024298d0
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m53s
Build and Release / Unit Tests (push) Successful in 8m48s
Build and Release / Lint (push) Successful in 9m12s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m35s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m50s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m2s
Build and Release / Build Binary (linux/arm64) (push) Failing after 14m53s
feat(pages): add JSON struct tags to pages config types
Add json struct tags to all LandingConfig types for proper JSON serialization in v2 API. Uses omitempty for primitives and omitzero for structs to exclude empty values from JSON output. Enables clean JSON responses from GET /repos/{owner}/{repo}/pages/config API endpoint.
2026-03-17 20:27:34 -04:00

472 lines
19 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" 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 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" json:"custom_css,omitempty"`
CustomHead string `yaml:"custom_head,omitempty" json:"custom_head,omitempty"`
Redirects map[string]string `yaml:"redirects,omitempty" json:"redirects,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",
}
}