Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m44s
Build and Release / Lint (push) Failing after 8m24s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
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
Build and Release / Unit Tests (push) Successful in 8m43s
Add comprehensive REST API for managing landing page configuration programmatically. Includes GET /repos/{owner}/{repo}/pages/config to retrieve full config, PUT to replace entire config, PATCH to update specific sections. Supports all config sections: brand, hero, stats, features, pricing, blog, gallery, comparison, etc. Adds UpdatePagesConfigOption and related structs for API payloads. Includes error codes for pages validation. Enables headless/automated landing page management.
839 lines
22 KiB
Go
839 lines
22 KiB
Go
// Copyright 2026 MarketAlly. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package v2
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
|
apierrors "code.gitcaddy.com/server/v3/modules/errors"
|
|
"code.gitcaddy.com/server/v3/modules/git"
|
|
pages_module "code.gitcaddy.com/server/v3/modules/pages"
|
|
api "code.gitcaddy.com/server/v3/modules/structs"
|
|
"code.gitcaddy.com/server/v3/modules/web"
|
|
"code.gitcaddy.com/server/v3/services/context"
|
|
pages_service "code.gitcaddy.com/server/v3/services/pages"
|
|
)
|
|
|
|
// PagesFullConfigResponse represents the complete landing page configuration
|
|
type PagesFullConfigResponse struct {
|
|
Enabled bool `json:"enabled"`
|
|
PublicLanding bool `json:"public_landing"`
|
|
Template string `json:"template"`
|
|
Domain string `json:"domain,omitempty"`
|
|
Brand pages_module.BrandConfig `json:"brand"`
|
|
Hero pages_module.HeroConfig `json:"hero"`
|
|
Stats []pages_module.StatConfig `json:"stats"`
|
|
ValueProps []pages_module.ValuePropConfig `json:"value_props"`
|
|
Features []pages_module.FeatureConfig `json:"features"`
|
|
SocialProof pages_module.SocialProofConfig `json:"social_proof"`
|
|
Pricing pages_module.PricingConfig `json:"pricing"`
|
|
CTASection pages_module.CTASectionConfig `json:"cta_section"`
|
|
Blog pages_module.BlogSectionConfig `json:"blog"`
|
|
Gallery pages_module.GallerySectionConfig `json:"gallery"`
|
|
Comparison pages_module.ComparisonSectionConfig `json:"comparison"`
|
|
Navigation pages_module.NavigationConfig `json:"navigation"`
|
|
Footer pages_module.FooterConfig `json:"footer"`
|
|
Theme pages_module.ThemeConfig `json:"theme"`
|
|
SEO pages_module.SEOConfig `json:"seo"`
|
|
Analytics pages_module.AnalyticsConfig `json:"analytics"`
|
|
Advanced pages_module.AdvancedConfig `json:"advanced"`
|
|
}
|
|
|
|
// PagesContentResponse represents the rendered content for a landing page
|
|
type PagesContentResponse struct {
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Readme string `json:"readme,omitempty"`
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func buildFullResponse(config *pages_module.LandingConfig) *PagesFullConfigResponse {
|
|
return &PagesFullConfigResponse{
|
|
Enabled: config.Enabled,
|
|
PublicLanding: config.PublicLanding,
|
|
Template: config.Template,
|
|
Domain: config.Domain,
|
|
Brand: config.Brand,
|
|
Hero: config.Hero,
|
|
Stats: config.Stats,
|
|
ValueProps: config.ValueProps,
|
|
Features: config.Features,
|
|
SocialProof: config.SocialProof,
|
|
Pricing: config.Pricing,
|
|
CTASection: config.CTASection,
|
|
Blog: config.Blog,
|
|
Gallery: config.Gallery,
|
|
Comparison: config.Comparison,
|
|
Navigation: config.Navigation,
|
|
Footer: config.Footer,
|
|
Theme: config.Theme,
|
|
SEO: config.SEO,
|
|
Analytics: config.Analytics,
|
|
Advanced: config.Advanced,
|
|
}
|
|
}
|
|
|
|
func getPagesConfigAPI(ctx *context.APIContext) (*pages_module.LandingConfig, bool) {
|
|
config, err := pages_service.GetPagesConfig(ctx, ctx.Repo.Repository)
|
|
if err != nil {
|
|
ctx.APIErrorWithCode(apierrors.PagesNotConfigured)
|
|
return nil, false
|
|
}
|
|
return config, true
|
|
}
|
|
|
|
func savePagesConfigAPI(ctx *context.APIContext, config *pages_module.LandingConfig) bool {
|
|
configJSON, err := json.Marshal(config)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return false
|
|
}
|
|
|
|
dbConfig, err := repo_model.GetPagesConfigByRepoID(ctx, ctx.Repo.Repository.ID)
|
|
if err != nil {
|
|
if repo_model.IsErrPagesConfigNotExist(err) {
|
|
dbConfig = &repo_model.PagesConfig{
|
|
RepoID: ctx.Repo.Repository.ID,
|
|
Enabled: config.Enabled,
|
|
Template: repo_model.PagesTemplate(config.Template),
|
|
ConfigJSON: string(configJSON),
|
|
}
|
|
if err := repo_model.CreatePagesConfig(ctx, dbConfig); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
ctx.APIErrorInternal(err)
|
|
return false
|
|
}
|
|
|
|
dbConfig.Enabled = config.Enabled
|
|
dbConfig.Template = repo_model.PagesTemplate(config.Template)
|
|
dbConfig.ConfigJSON = string(configJSON)
|
|
if err := repo_model.UpdatePagesConfig(ctx, dbConfig); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func requirePagesAdmin(ctx *context.APIContext) bool {
|
|
if !ctx.Repo.Permission.IsAdmin() && !ctx.IsUserSiteAdmin() {
|
|
ctx.APIErrorWithCode(apierrors.PermRepoAdminRequired)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Apply helpers — map API option structs to config structs
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func applyCTAButton(dst *pages_module.CTAButton, src *api.PagesCTAButtonOption) {
|
|
if src == nil {
|
|
return
|
|
}
|
|
if src.Label != nil {
|
|
dst.Label = *src.Label
|
|
}
|
|
if src.URL != nil {
|
|
dst.URL = *src.URL
|
|
}
|
|
if src.Variant != nil {
|
|
dst.Variant = *src.Variant
|
|
}
|
|
}
|
|
|
|
func applyBrand(dst *pages_module.BrandConfig, src *api.UpdatePagesBrandOption) {
|
|
if src.Name != nil {
|
|
dst.Name = *src.Name
|
|
}
|
|
if src.LogoURL != nil {
|
|
dst.LogoURL = *src.LogoURL
|
|
}
|
|
if src.Tagline != nil {
|
|
dst.Tagline = *src.Tagline
|
|
}
|
|
if src.FaviconURL != nil {
|
|
dst.FaviconURL = *src.FaviconURL
|
|
}
|
|
}
|
|
|
|
func applyHero(dst *pages_module.HeroConfig, src *api.UpdatePagesHeroOption) {
|
|
if src.Headline != nil {
|
|
dst.Headline = *src.Headline
|
|
}
|
|
if src.Subheadline != nil {
|
|
dst.Subheadline = *src.Subheadline
|
|
}
|
|
if src.ImageURL != nil {
|
|
dst.ImageURL = *src.ImageURL
|
|
}
|
|
if src.VideoURL != nil {
|
|
dst.VideoURL = *src.VideoURL
|
|
}
|
|
if src.CodeExample != nil {
|
|
dst.CodeExample = *src.CodeExample
|
|
}
|
|
applyCTAButton(&dst.PrimaryCTA, src.PrimaryCTA)
|
|
applyCTAButton(&dst.SecondaryCTA, src.SecondaryCTA)
|
|
}
|
|
|
|
func applyStats(config *pages_module.LandingConfig, src []api.PagesStatOption) {
|
|
config.Stats = make([]pages_module.StatConfig, len(src))
|
|
for i, s := range src {
|
|
config.Stats[i] = pages_module.StatConfig{Value: s.Value, Label: s.Label}
|
|
}
|
|
}
|
|
|
|
func applyValueProps(config *pages_module.LandingConfig, src []api.PagesValuePropOption) {
|
|
config.ValueProps = make([]pages_module.ValuePropConfig, len(src))
|
|
for i, v := range src {
|
|
config.ValueProps[i] = pages_module.ValuePropConfig{Title: v.Title, Description: v.Description, Icon: v.Icon}
|
|
}
|
|
}
|
|
|
|
func applyFeatures(config *pages_module.LandingConfig, src []api.PagesFeatureOption) {
|
|
config.Features = make([]pages_module.FeatureConfig, len(src))
|
|
for i, f := range src {
|
|
config.Features[i] = pages_module.FeatureConfig{Title: f.Title, Description: f.Description, Icon: f.Icon, ImageURL: f.ImageURL}
|
|
}
|
|
}
|
|
|
|
func applySocial(dst *pages_module.SocialProofConfig, src *api.UpdatePagesSocialOption) {
|
|
if src.Logos != nil {
|
|
dst.Logos = *src.Logos
|
|
}
|
|
if src.Testimonials != nil {
|
|
dst.Testimonials = make([]pages_module.TestimonialConfig, len(*src.Testimonials))
|
|
for i, t := range *src.Testimonials {
|
|
dst.Testimonials[i] = pages_module.TestimonialConfig{Quote: t.Quote, Author: t.Author, Role: t.Role, Avatar: t.Avatar}
|
|
}
|
|
}
|
|
}
|
|
|
|
func applyPricing(dst *pages_module.PricingConfig, src *api.UpdatePagesPricingOption) {
|
|
if src.Headline != nil {
|
|
dst.Headline = *src.Headline
|
|
}
|
|
if src.Subheadline != nil {
|
|
dst.Subheadline = *src.Subheadline
|
|
}
|
|
if src.Plans != nil {
|
|
dst.Plans = make([]pages_module.PricingPlanConfig, len(*src.Plans))
|
|
for i, p := range *src.Plans {
|
|
dst.Plans[i] = pages_module.PricingPlanConfig{
|
|
Name: p.Name, Price: p.Price, Period: p.Period,
|
|
Features: p.Features, CTA: p.CTA, Featured: p.Featured,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func applyCTASection(dst *pages_module.CTASectionConfig, src *api.UpdatePagesCTAOption) {
|
|
if src.Headline != nil {
|
|
dst.Headline = *src.Headline
|
|
}
|
|
if src.Subheadline != nil {
|
|
dst.Subheadline = *src.Subheadline
|
|
}
|
|
applyCTAButton(&dst.Button, src.Button)
|
|
}
|
|
|
|
func applyBlog(dst *pages_module.BlogSectionConfig, src *api.UpdatePagesBlogOption) {
|
|
if src.Enabled != nil {
|
|
dst.Enabled = *src.Enabled
|
|
}
|
|
if src.Headline != nil {
|
|
dst.Headline = *src.Headline
|
|
}
|
|
if src.Subheadline != nil {
|
|
dst.Subheadline = *src.Subheadline
|
|
}
|
|
if src.MaxPosts != nil {
|
|
dst.MaxPosts = *src.MaxPosts
|
|
}
|
|
}
|
|
|
|
func applyGallery(dst *pages_module.GallerySectionConfig, src *api.UpdatePagesGalleryOption) {
|
|
if src.Enabled != nil {
|
|
dst.Enabled = *src.Enabled
|
|
}
|
|
if src.Headline != nil {
|
|
dst.Headline = *src.Headline
|
|
}
|
|
if src.Subheadline != nil {
|
|
dst.Subheadline = *src.Subheadline
|
|
}
|
|
if src.MaxImages != nil {
|
|
dst.MaxImages = *src.MaxImages
|
|
}
|
|
if src.Columns != nil {
|
|
dst.Columns = *src.Columns
|
|
}
|
|
}
|
|
|
|
func applyComparison(dst *pages_module.ComparisonSectionConfig, src *api.UpdatePagesComparisonOption) {
|
|
if src.Enabled != nil {
|
|
dst.Enabled = *src.Enabled
|
|
}
|
|
if src.Headline != nil {
|
|
dst.Headline = *src.Headline
|
|
}
|
|
if src.Subheadline != nil {
|
|
dst.Subheadline = *src.Subheadline
|
|
}
|
|
if src.Columns != nil {
|
|
dst.Columns = make([]pages_module.ComparisonColumnConfig, len(*src.Columns))
|
|
for i, c := range *src.Columns {
|
|
dst.Columns[i] = pages_module.ComparisonColumnConfig{Name: c.Name, Highlight: c.Highlight}
|
|
}
|
|
}
|
|
if src.Groups != nil {
|
|
dst.Groups = make([]pages_module.ComparisonGroupConfig, len(*src.Groups))
|
|
for i, g := range *src.Groups {
|
|
features := make([]pages_module.ComparisonFeatureConfig, len(g.Features))
|
|
for j, f := range g.Features {
|
|
features[j] = pages_module.ComparisonFeatureConfig{Name: f.Name, Values: f.Values}
|
|
}
|
|
dst.Groups[i] = pages_module.ComparisonGroupConfig{Name: g.Name, Features: features}
|
|
}
|
|
}
|
|
}
|
|
|
|
func applyNavigation(dst *pages_module.NavigationConfig, src *api.UpdatePagesNavOption) {
|
|
if src.ShowDocs != nil {
|
|
dst.ShowDocs = *src.ShowDocs
|
|
}
|
|
if src.ShowAPI != nil {
|
|
dst.ShowAPI = *src.ShowAPI
|
|
}
|
|
if src.ShowRepository != nil {
|
|
dst.ShowRepository = *src.ShowRepository
|
|
}
|
|
if src.ShowReleases != nil {
|
|
dst.ShowReleases = *src.ShowReleases
|
|
}
|
|
if src.ShowIssues != nil {
|
|
dst.ShowIssues = *src.ShowIssues
|
|
}
|
|
}
|
|
|
|
func applyFooter(dst *pages_module.FooterConfig, ctaDst *pages_module.CTASectionConfig, src *api.UpdatePagesFooterOption) {
|
|
if src.Copyright != nil {
|
|
dst.Copyright = *src.Copyright
|
|
}
|
|
if src.ShowPoweredBy != nil {
|
|
dst.ShowPoweredBy = *src.ShowPoweredBy
|
|
}
|
|
if src.Links != nil {
|
|
dst.Links = make([]pages_module.FooterLink, len(*src.Links))
|
|
for i, l := range *src.Links {
|
|
dst.Links[i] = pages_module.FooterLink{Label: l.Label, URL: l.URL}
|
|
}
|
|
}
|
|
if src.Social != nil {
|
|
dst.Social = make([]pages_module.SocialLink, len(*src.Social))
|
|
for i, s := range *src.Social {
|
|
dst.Social[i] = pages_module.SocialLink{Platform: s.Platform, URL: s.URL}
|
|
}
|
|
}
|
|
if src.CTASection != nil {
|
|
applyCTASection(ctaDst, src.CTASection)
|
|
}
|
|
}
|
|
|
|
func applyTheme(dst *pages_module.ThemeConfig, src *api.UpdatePagesThemeOption) {
|
|
if src.PrimaryColor != nil {
|
|
dst.PrimaryColor = *src.PrimaryColor
|
|
}
|
|
if src.AccentColor != nil {
|
|
dst.AccentColor = *src.AccentColor
|
|
}
|
|
if src.Mode != nil {
|
|
dst.Mode = *src.Mode
|
|
}
|
|
}
|
|
|
|
func applySEO(dst *pages_module.SEOConfig, src *api.UpdatePagesSEOOption) {
|
|
if src.Title != nil {
|
|
dst.Title = *src.Title
|
|
}
|
|
if src.Description != nil {
|
|
dst.Description = *src.Description
|
|
}
|
|
if src.Keywords != nil {
|
|
dst.Keywords = *src.Keywords
|
|
}
|
|
if src.OGImage != nil {
|
|
dst.OGImage = *src.OGImage
|
|
}
|
|
if src.UseMediaKitOG != nil {
|
|
dst.UseMediaKitOG = *src.UseMediaKitOG
|
|
}
|
|
if src.TwitterCard != nil {
|
|
dst.TwitterCard = *src.TwitterCard
|
|
}
|
|
if src.TwitterSite != nil {
|
|
dst.TwitterSite = *src.TwitterSite
|
|
}
|
|
}
|
|
|
|
func applyAdvanced(dst *pages_module.AdvancedConfig, src *api.UpdatePagesAdvancedOption) {
|
|
if src.CustomCSS != nil {
|
|
dst.CustomCSS = *src.CustomCSS
|
|
}
|
|
if src.CustomHead != nil {
|
|
dst.CustomHead = *src.CustomHead
|
|
}
|
|
if src.PublicReleases != nil {
|
|
dst.PublicReleases = *src.PublicReleases
|
|
}
|
|
if src.HideMobileReleases != nil {
|
|
dst.HideMobileReleases = *src.HideMobileReleases
|
|
}
|
|
if src.GooglePlayID != nil {
|
|
dst.GooglePlayID = *src.GooglePlayID
|
|
}
|
|
if src.AppStoreID != nil {
|
|
dst.AppStoreID = *src.AppStoreID
|
|
}
|
|
}
|
|
|
|
// applyFullConfig applies all non-nil sections from the update option to the config
|
|
func applyFullConfig(config *pages_module.LandingConfig, form *api.UpdatePagesConfigOption) {
|
|
if form.Enabled != nil {
|
|
config.Enabled = *form.Enabled
|
|
}
|
|
if form.PublicLanding != nil {
|
|
config.PublicLanding = *form.PublicLanding
|
|
}
|
|
if form.Template != nil {
|
|
config.Template = *form.Template
|
|
}
|
|
if form.Brand != nil {
|
|
applyBrand(&config.Brand, form.Brand)
|
|
}
|
|
if form.Hero != nil {
|
|
applyHero(&config.Hero, form.Hero)
|
|
}
|
|
if form.Stats != nil {
|
|
applyStats(config, *form.Stats)
|
|
}
|
|
if form.ValueProps != nil {
|
|
applyValueProps(config, *form.ValueProps)
|
|
}
|
|
if form.Features != nil {
|
|
applyFeatures(config, *form.Features)
|
|
}
|
|
if form.SocialProof != nil {
|
|
applySocial(&config.SocialProof, form.SocialProof)
|
|
}
|
|
if form.Pricing != nil {
|
|
applyPricing(&config.Pricing, form.Pricing)
|
|
}
|
|
if form.CTASection != nil {
|
|
applyCTASection(&config.CTASection, form.CTASection)
|
|
}
|
|
if form.Blog != nil {
|
|
applyBlog(&config.Blog, form.Blog)
|
|
}
|
|
if form.Gallery != nil {
|
|
applyGallery(&config.Gallery, form.Gallery)
|
|
}
|
|
if form.Comparison != nil {
|
|
applyComparison(&config.Comparison, form.Comparison)
|
|
}
|
|
if form.Navigation != nil {
|
|
applyNavigation(&config.Navigation, form.Navigation)
|
|
}
|
|
if form.Footer != nil {
|
|
applyFooter(&config.Footer, &config.CTASection, form.Footer)
|
|
}
|
|
if form.Theme != nil {
|
|
applyTheme(&config.Theme, form.Theme)
|
|
}
|
|
if form.SEO != nil {
|
|
applySEO(&config.SEO, form.SEO)
|
|
}
|
|
if form.Advanced != nil {
|
|
applyAdvanced(&config.Advanced, form.Advanced)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET endpoints
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// GetPagesConfig returns the full pages configuration for a repository
|
|
// GET /api/v2/repos/{owner}/{repo}/pages/config
|
|
func GetPagesConfig(ctx *context.APIContext) {
|
|
repo := ctx.Repo.Repository
|
|
if repo == nil {
|
|
ctx.APIErrorNotFound("Repository not found")
|
|
return
|
|
}
|
|
|
|
config, err := pages_service.GetPagesConfig(ctx, repo)
|
|
if err != nil {
|
|
ctx.APIErrorWithCode(apierrors.PagesNotConfigured)
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
|
}
|
|
|
|
// GetPagesContent returns the rendered content for a repository's landing page
|
|
// GET /api/v2/repos/{owner}/{repo}/pages/content
|
|
func GetPagesContent(ctx *context.APIContext) {
|
|
repo := ctx.Repo.Repository
|
|
if repo == nil {
|
|
ctx.APIErrorNotFound("Repository not found")
|
|
return
|
|
}
|
|
|
|
config, err := pages_service.GetPagesConfig(ctx, repo)
|
|
if err != nil || !config.Enabled {
|
|
ctx.APIErrorWithCode(apierrors.PagesNotEnabled)
|
|
return
|
|
}
|
|
|
|
readme := loadReadmeContent(ctx, repo)
|
|
|
|
title := config.SEO.Title
|
|
if title == "" {
|
|
title = config.Hero.Headline
|
|
}
|
|
if title == "" {
|
|
title = config.Brand.Name
|
|
}
|
|
if title == "" {
|
|
title = repo.Name
|
|
}
|
|
|
|
description := config.SEO.Description
|
|
if description == "" {
|
|
description = config.Hero.Subheadline
|
|
}
|
|
if description == "" {
|
|
description = repo.Description
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, &PagesContentResponse{
|
|
Title: title,
|
|
Description: description,
|
|
Readme: readme,
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PUT /config — replace full config
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// UpdatePagesConfig replaces the entire landing page configuration
|
|
// PUT /api/v2/repos/{owner}/{repo}/pages/config
|
|
func UpdatePagesConfig(ctx *context.APIContext) {
|
|
if !requirePagesAdmin(ctx) {
|
|
return
|
|
}
|
|
config, ok := getPagesConfigAPI(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
form := web.GetForm(ctx).(*api.UpdatePagesConfigOption)
|
|
|
|
if form.Template != nil && !pages_module.IsValidTemplate(*form.Template) {
|
|
ctx.APIErrorWithCode(apierrors.PagesInvalidTemplate)
|
|
return
|
|
}
|
|
|
|
applyFullConfig(config, form)
|
|
|
|
if !savePagesConfigAPI(ctx, config) {
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PATCH /config — partial merge
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// PatchPagesConfig partially updates the landing page configuration
|
|
// PATCH /api/v2/repos/{owner}/{repo}/pages/config
|
|
func PatchPagesConfig(ctx *context.APIContext) {
|
|
if !requirePagesAdmin(ctx) {
|
|
return
|
|
}
|
|
config, ok := getPagesConfigAPI(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
form := web.GetForm(ctx).(*api.UpdatePagesConfigOption)
|
|
|
|
if form.Template != nil && !pages_module.IsValidTemplate(*form.Template) {
|
|
ctx.APIErrorWithCode(apierrors.PagesInvalidTemplate)
|
|
return
|
|
}
|
|
|
|
applyFullConfig(config, form)
|
|
|
|
if !savePagesConfigAPI(ctx, config) {
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Section PUT endpoints
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// UpdatePagesBrand updates the brand section
|
|
// PUT /api/v2/repos/{owner}/{repo}/pages/config/brand
|
|
func UpdatePagesBrand(ctx *context.APIContext) {
|
|
if !requirePagesAdmin(ctx) {
|
|
return
|
|
}
|
|
config, ok := getPagesConfigAPI(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
form := web.GetForm(ctx).(*api.UpdatePagesBrandOption)
|
|
applyBrand(&config.Brand, form)
|
|
|
|
if !savePagesConfigAPI(ctx, config) {
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
|
}
|
|
|
|
// UpdatePagesHero updates the hero section
|
|
// PUT /api/v2/repos/{owner}/{repo}/pages/config/hero
|
|
func UpdatePagesHero(ctx *context.APIContext) {
|
|
if !requirePagesAdmin(ctx) {
|
|
return
|
|
}
|
|
config, ok := getPagesConfigAPI(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
form := web.GetForm(ctx).(*api.UpdatePagesHeroOption)
|
|
applyHero(&config.Hero, form)
|
|
|
|
if !savePagesConfigAPI(ctx, config) {
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
|
}
|
|
|
|
// UpdatePagesContentSection updates the content section (blog, gallery, stats, features, nav, etc.)
|
|
// PUT /api/v2/repos/{owner}/{repo}/pages/config/content
|
|
func UpdatePagesContentSection(ctx *context.APIContext) {
|
|
if !requirePagesAdmin(ctx) {
|
|
return
|
|
}
|
|
config, ok := getPagesConfigAPI(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
form := web.GetForm(ctx).(*api.UpdatePagesContentOption)
|
|
|
|
if form.Blog != nil {
|
|
applyBlog(&config.Blog, form.Blog)
|
|
}
|
|
if form.Gallery != nil {
|
|
applyGallery(&config.Gallery, form.Gallery)
|
|
}
|
|
if form.ComparisonEnabled != nil {
|
|
config.Comparison.Enabled = *form.ComparisonEnabled
|
|
}
|
|
if form.Stats != nil {
|
|
applyStats(config, *form.Stats)
|
|
}
|
|
if form.ValueProps != nil {
|
|
applyValueProps(config, *form.ValueProps)
|
|
}
|
|
if form.Features != nil {
|
|
applyFeatures(config, *form.Features)
|
|
}
|
|
if form.Navigation != nil {
|
|
applyNavigation(&config.Navigation, form.Navigation)
|
|
}
|
|
if form.Advanced != nil {
|
|
applyAdvanced(&config.Advanced, form.Advanced)
|
|
}
|
|
|
|
if !savePagesConfigAPI(ctx, config) {
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
|
}
|
|
|
|
// UpdatePagesComparison updates the comparison section
|
|
// PUT /api/v2/repos/{owner}/{repo}/pages/config/comparison
|
|
func UpdatePagesComparison(ctx *context.APIContext) {
|
|
if !requirePagesAdmin(ctx) {
|
|
return
|
|
}
|
|
config, ok := getPagesConfigAPI(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
form := web.GetForm(ctx).(*api.UpdatePagesComparisonOption)
|
|
applyComparison(&config.Comparison, form)
|
|
|
|
if !savePagesConfigAPI(ctx, config) {
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
|
}
|
|
|
|
// UpdatePagesSocial updates the social proof section
|
|
// PUT /api/v2/repos/{owner}/{repo}/pages/config/social
|
|
func UpdatePagesSocial(ctx *context.APIContext) {
|
|
if !requirePagesAdmin(ctx) {
|
|
return
|
|
}
|
|
config, ok := getPagesConfigAPI(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
form := web.GetForm(ctx).(*api.UpdatePagesSocialOption)
|
|
applySocial(&config.SocialProof, form)
|
|
|
|
if !savePagesConfigAPI(ctx, config) {
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
|
}
|
|
|
|
// UpdatePagesPricing updates the pricing section
|
|
// PUT /api/v2/repos/{owner}/{repo}/pages/config/pricing
|
|
func UpdatePagesPricing(ctx *context.APIContext) {
|
|
if !requirePagesAdmin(ctx) {
|
|
return
|
|
}
|
|
config, ok := getPagesConfigAPI(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
form := web.GetForm(ctx).(*api.UpdatePagesPricingOption)
|
|
applyPricing(&config.Pricing, form)
|
|
|
|
if !savePagesConfigAPI(ctx, config) {
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
|
}
|
|
|
|
// UpdatePagesFooter updates the footer and CTA section
|
|
// PUT /api/v2/repos/{owner}/{repo}/pages/config/footer
|
|
func UpdatePagesFooter(ctx *context.APIContext) {
|
|
if !requirePagesAdmin(ctx) {
|
|
return
|
|
}
|
|
config, ok := getPagesConfigAPI(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
form := web.GetForm(ctx).(*api.UpdatePagesFooterOption)
|
|
applyFooter(&config.Footer, &config.CTASection, form)
|
|
|
|
if !savePagesConfigAPI(ctx, config) {
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
|
}
|
|
|
|
// UpdatePagesTheme updates the theme and SEO section
|
|
// PUT /api/v2/repos/{owner}/{repo}/pages/config/theme
|
|
func UpdatePagesTheme(ctx *context.APIContext) {
|
|
if !requirePagesAdmin(ctx) {
|
|
return
|
|
}
|
|
config, ok := getPagesConfigAPI(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
form := web.GetForm(ctx).(*api.UpdatePagesThemeOption)
|
|
applyTheme(&config.Theme, form)
|
|
|
|
if !savePagesConfigAPI(ctx, config) {
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Utility
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// loadReadmeContent loads the README content from the repository
|
|
func loadReadmeContent(ctx *context.APIContext, repo *repo_model.Repository) string {
|
|
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer gitRepo.Close()
|
|
|
|
branch := repo.DefaultBranch
|
|
if branch == "" {
|
|
branch = "main"
|
|
}
|
|
|
|
commit, err := gitRepo.GetBranchCommit(branch)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
readmePaths := []string{
|
|
"README.md",
|
|
"readme.md",
|
|
"Readme.md",
|
|
"README.markdown",
|
|
"README.txt",
|
|
"README",
|
|
}
|
|
|
|
for _, path := range readmePaths {
|
|
entry, err := commit.GetTreeEntryByPath(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
reader, err := entry.Blob().DataAsync()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
content := make([]byte, entry.Blob().Size())
|
|
_, err = reader.Read(content)
|
|
reader.Close()
|
|
|
|
if err != nil && err.Error() != "EOF" {
|
|
continue
|
|
}
|
|
|
|
return string(content)
|
|
}
|
|
|
|
return ""
|
|
}
|