2
0

10 Commits

Author SHA1 Message Date
c5daac3366 feat(mcp): add stats, value props, and CTA tools for landing pages
All checks were successful
Build and Release / Lint (push) Successful in 5m21s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m21s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m32s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 12m1s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m55s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h7m31s
Build and Release / Unit Tests (push) Successful in 14m12s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 12m51s
Implements three new MCP tools for landing page management: update_landing_stats for stat counters, update_landing_value_props for value proposition cards, and update_landing_cta for bottom call-to-action section. Each tool supports structured data with validation and integrates with existing config save flow.
2026-04-05 12:49:19 -04:00
916211004d docs(mcp): add explanation to nolint directive
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m21s
Build and Release / Unit Tests (push) Successful in 15m52s
Build and Release / Lint (push) Successful in 16m53s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m18s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m49s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 8m12s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h7m30s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m19s
Clarifies why unparam is suppressed on toolListLandingTemplates with inline comment explaining interface requirement.
2026-04-05 09:09:01 -04:00
02fdc1a194 chore(mcp): suppress unparam linter warning for toolListLandingTemplates
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m27s
Build and Release / Unit Tests (push) Successful in 5m15s
Build and Release / Lint (push) Failing after 10m56s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (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
Adds nolint:unparam directive to toolListLandingTemplates. Function signature must match tool handler interface even though parameters are unused.
2026-04-05 08:45:31 -04:00
1b0bba09b9 style(mcp): use errors.New for static error messages
Some checks failed
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m19s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Lint (push) Failing after 11m49s
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 / 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 12m52s
Replaces fmt.Errorf with errors.New for static error message in resolveOwnerRepo. Marks unused context and args parameters with underscore in toolListLandingTemplates.
2026-04-05 04:03:02 -04:00
0c0d1c1493 feat(mcp): add landing page management tools to MCP server
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Unit Tests (push) Successful in 3m42s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m25s
Build and Release / Lint (push) Failing after 10m12s
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
Creates mcp_pages.go with 10 new tools for managing repository landing pages via Claude Code. Supports getting/updating brand, hero, pricing, comparison, features, social proof, SEO, and theme sections. Includes template listing and enable/disable functionality. Integrates with existing pages service and registers tools in handleToolsList and handleToolsCall.
2026-04-05 03:39:54 -04:00
9461599b57 fix(api): populate Repo field in release API responses
All checks were successful
Build and Release / Unit Tests (push) Successful in 7m8s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m33s
Build and Release / Lint (push) Successful in 7m50s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m31s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m39s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m34s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 12m17s
Build and Release / Build Binary (linux/arm64) (push) Successful in 33m20s
Set release.Repo before converting to API format in all v2 release endpoints (CheckAppUpdate, ListReleasesV2, GetReleaseV2, GetLatestReleaseV2). Ensures repository information is available in API responses
2026-04-03 23:57:29 -04:00
414560f470 fix(ci): isolate GOMODCACHE per job to prevent cache conflicts
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m1s
Build and Release / Unit Tests (push) Successful in 22m31s
Build and Release / Lint (push) Successful in 24m45s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m37s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m40s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h7m8s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 13m32s
Build and Release / Build Binary (linux/arm64) (push) Failing after 30m51s
Move GOMODCACHE from global env to job-level env with unique paths per job. Prevents race conditions and cache corruption when jobs run in parallel, especially after the gotextdiff mirror replacement.
2026-03-30 09:47:38 -04:00
b43345986a refactor(pages): remove unused app store fields from advanced settings
Some checks failed
Build and Release / Unit Tests (push) Successful in 6m20s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Lint (push) Successful in 6m31s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Failing after 9h1m42s
Build and Release / Integration Tests (PostgreSQL) (push) Has been cancelled
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been cancelled
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
2026-03-30 09:32:58 -04:00
7fbbd26b20 refactor(ui): consolidate pages navigation into main settings navbar
Some checks failed
Build and Release / Unit Tests (push) Successful in 7m33s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m40s
Build and Release / Lint (push) Successful in 7m57s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Failing after 9h1m36s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 7m8s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 7m21s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m37s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 20m50s
Remove redundant pages_nav.tmpl and integrate the advanced pages link directly into the main settings navbar. Reduces template duplication while maintaining the same navigation structure.
2026-03-30 03:23:26 -04:00
b26bf4bfe8 fix(ci): replace deleted gotextdiff dependency with mirror
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Failing after 1m41s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m32s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been cancelled
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Lint (push) Has been cancelled
The upstream github.com/hexops/gotextdiff repository was deleted. Replace with internal mirror and add go clean -modcache to all CI dependency installation steps to ensure clean builds with the new module path.
2026-03-30 03:12:19 -04:00
10 changed files with 852 additions and 76 deletions

View File

@@ -24,6 +24,8 @@ jobs:
lint:
name: Lint
runs-on: linux-latest
env:
GOMODCACHE: /tmp/gomod-${{ github.run_id }}-lint
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -51,7 +53,8 @@ jobs:
run: npm install -g pnpm
- name: Install dependencies
run: make deps-frontend deps-backend
run: |
make deps-frontend deps-backend
- name: Run Go linter
run: make lint-go
@@ -64,6 +67,8 @@ jobs:
test-unit:
name: Unit Tests
runs-on: linux-latest
env:
GOMODCACHE: /tmp/gomod-${{ github.run_id }}-unit
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -83,7 +88,8 @@ jobs:
cache: false
- name: Install dependencies
run: go mod download
run: |
go mod download
- name: Run unit tests
run: |
@@ -99,6 +105,8 @@ jobs:
test-pgsql:
name: Integration Tests (PostgreSQL)
runs-on: linux-latest
env:
GOMODCACHE: /tmp/gomod-${{ github.run_id }}-pgsql
services:
pgsql:
image: postgres:15
@@ -140,7 +148,8 @@ jobs:
run: npm install -g pnpm
- name: Install dependencies
run: make deps-frontend deps-backend
run: |
make deps-frontend deps-backend
- name: Build frontend
run: make frontend
@@ -378,7 +387,8 @@ jobs:
- name: Install dependencies (Unix)
if: matrix.goos != 'windows'
run: make deps-frontend deps-backend
run: |
make deps-frontend deps-backend
- name: Install dependencies (Windows)
if: matrix.goos == 'windows'

3
go.mod
View File

@@ -326,6 +326,9 @@ replace github.com/go-ini/ini => github.com/go-ini/ini v1.66.6
// Use GitCaddy fork with capability support
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.8
// Mirror of deleted github.com/hexops/gotextdiff
replace github.com/hexops/gotextdiff => git.marketally.com/mirrors/gotextdiff v1.0.3
// Vault plugin - use local directory for development
replace git.marketally.com/gitcaddy/gitcaddy-vault => ../gitcaddy-vault

4
go.sum
View File

@@ -31,6 +31,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.marketally.com/gitcaddy/actions-proto-go v0.5.8 h1:MBipeHvY6A0jcobvziUtzgatZTrV4fs/HE1rPQxREN4=
git.marketally.com/gitcaddy/actions-proto-go v0.5.8/go.mod h1:RPu21UoRD3zSAujoZR6LJwuVNa2uFRBveadslczCRfQ=
git.marketally.com/mirrors/gotextdiff v1.0.3 h1:Mxf+YurdCHT4y1GNiZCTDWYtVXSxhlLUeG7g7i9Za70=
git.marketally.com/mirrors/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c=
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
@@ -490,8 +492,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=

View File

@@ -685,9 +685,10 @@ func handleInitialize(ctx *context_service.APIContext, req *MCPRequest) {
}
func handleToolsList(ctx *context_service.APIContext, req *MCPRequest) {
allTools := make([]MCPTool, 0, len(mcpTools)+len(mcpAITools))
allTools := make([]MCPTool, 0, len(mcpTools)+len(mcpAITools)+len(mcpPagesTools))
allTools = append(allTools, mcpTools...)
allTools = append(allTools, mcpAITools...)
allTools = append(allTools, mcpPagesTools...)
result := MCPToolsListResult{Tools: allTools}
sendMCPResult(ctx, req.ID, result)
}
@@ -759,6 +760,35 @@ func handleToolsCall(ctx *context_service.APIContext, req *MCPRequest) {
result, err = toolListIssues(ctx, params.Arguments)
case "get_issue":
result, err = toolGetIssue(ctx, params.Arguments)
// Landing Pages tools
case "get_landing_config":
result, err = toolGetLandingConfig(ctx, params.Arguments)
case "list_landing_templates":
result, err = toolListLandingTemplates(ctx, params.Arguments)
case "enable_landing_page":
result, err = toolEnableLandingPage(ctx, params.Arguments)
case "update_landing_brand":
result, err = toolUpdateLandingBrand(ctx, params.Arguments)
case "update_landing_hero":
result, err = toolUpdateLandingHero(ctx, params.Arguments)
case "update_landing_pricing":
result, err = toolUpdateLandingPricing(ctx, params.Arguments)
case "update_landing_comparison":
result, err = toolUpdateLandingComparison(ctx, params.Arguments)
case "update_landing_features":
result, err = toolUpdateLandingFeatures(ctx, params.Arguments)
case "update_landing_social_proof":
result, err = toolUpdateLandingSocialProof(ctx, params.Arguments)
case "update_landing_seo":
result, err = toolUpdateLandingSEO(ctx, params.Arguments)
case "update_landing_theme":
result, err = toolUpdateLandingTheme(ctx, params.Arguments)
case "update_landing_stats":
result, err = toolUpdateLandingStats(ctx, params.Arguments)
case "update_landing_value_props":
result, err = toolUpdateLandingValueProps(ctx, params.Arguments)
case "update_landing_cta":
result, err = toolUpdateLandingCTA(ctx, params.Arguments)
default:
sendMCPError(ctx, req.ID, -32602, "Unknown tool", params.Name)
return

794
routers/api/v2/mcp_pages.go Normal file
View File

@@ -0,0 +1,794 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v2
import (
"errors"
"fmt"
repo_model "code.gitcaddy.com/server/v3/models/repo"
user_model "code.gitcaddy.com/server/v3/models/user"
"code.gitcaddy.com/server/v3/modules/json"
pages_module "code.gitcaddy.com/server/v3/modules/pages"
"code.gitcaddy.com/server/v3/services/context"
pages_service "code.gitcaddy.com/server/v3/services/pages"
)
// Landing Pages MCP Tools
var mcpPagesTools = []MCPTool{
{
Name: "get_landing_config",
Description: "Get the full landing page configuration for a repository. Returns all sections: brand, hero, pricing, comparison, features, social proof, SEO, and more.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"owner", "repo"},
"properties": map[string]any{
"owner": map[string]any{"type": "string", "description": "Repository owner"},
"repo": map[string]any{"type": "string", "description": "Repository name"},
},
},
},
{
Name: "list_landing_templates",
Description: "List available landing page templates with display names. Templates include: open-source-hero, saas-conversion, bold-marketing, developer-tool, and more.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{},
},
},
{
Name: "enable_landing_page",
Description: "Enable or disable the landing page for a repository. Optionally set a template.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"owner", "repo", "enabled"},
"properties": map[string]any{
"owner": map[string]any{"type": "string", "description": "Repository owner"},
"repo": map[string]any{"type": "string", "description": "Repository name"},
"enabled": map[string]any{"type": "boolean", "description": "Enable or disable"},
"template": map[string]any{"type": "string", "description": "Template name (e.g., 'saas-conversion', 'open-source-hero')"},
},
},
},
{
Name: "update_landing_brand",
Description: "Update the brand section of a landing page: name, logo URL, tagline, favicon.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"owner", "repo"},
"properties": map[string]any{
"owner": map[string]any{"type": "string", "description": "Repository owner"},
"repo": map[string]any{"type": "string", "description": "Repository name"},
"name": map[string]any{"type": "string", "description": "Brand name"},
"logo_url": map[string]any{"type": "string", "description": "Logo image URL"},
"tagline": map[string]any{"type": "string", "description": "Brand tagline"},
"favicon_url": map[string]any{"type": "string", "description": "Favicon URL"},
},
},
},
{
Name: "update_landing_hero",
Description: "Update the hero section: headline, subheadline, CTA buttons, hero image or video URL.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"owner", "repo"},
"properties": map[string]any{
"owner": map[string]any{"type": "string", "description": "Repository owner"},
"repo": map[string]any{"type": "string", "description": "Repository name"},
"headline": map[string]any{"type": "string", "description": "Main headline"},
"subheadline": map[string]any{"type": "string", "description": "Supporting text"},
"image_url": map[string]any{"type": "string", "description": "Hero image URL"},
"video_url": map[string]any{"type": "string", "description": "Hero video URL"},
"primary_cta_label": map[string]any{"type": "string", "description": "Primary CTA button label"},
"primary_cta_url": map[string]any{"type": "string", "description": "Primary CTA button URL"},
"secondary_cta_label": map[string]any{"type": "string", "description": "Secondary CTA button label"},
"secondary_cta_url": map[string]any{"type": "string", "description": "Secondary CTA button URL"},
},
},
},
{
Name: "update_landing_pricing",
Description: "Update the pricing section with plans. Each plan has a name, price, period, feature list, CTA, and optional 'featured' flag.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"owner", "repo", "plans"},
"properties": map[string]any{
"owner": map[string]any{"type": "string", "description": "Repository owner"},
"repo": map[string]any{"type": "string", "description": "Repository name"},
"headline": map[string]any{"type": "string", "description": "Pricing section headline"},
"subheadline": map[string]any{"type": "string", "description": "Pricing section subheadline"},
"plans": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
"price": map[string]any{"type": "string"},
"period": map[string]any{"type": "string"},
"features": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
"cta": map[string]any{"type": "string"},
"featured": map[string]any{"type": "boolean"},
},
},
"description": "Array of pricing plans",
},
},
},
},
{
Name: "update_landing_comparison",
Description: "Update the feature comparison matrix. Define columns (products/tiers) and groups of features with per-column values.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"owner", "repo"},
"properties": map[string]any{
"owner": map[string]any{"type": "string", "description": "Repository owner"},
"repo": map[string]any{"type": "string", "description": "Repository name"},
"enabled": map[string]any{"type": "boolean", "description": "Enable comparison section"},
"headline": map[string]any{"type": "string", "description": "Section headline"},
"subheadline": map[string]any{"type": "string", "description": "Section subheadline"},
"columns": map[string]any{
"type": "array",
"items": map[string]any{"type": "string"},
"description": "Column headers (e.g., ['Free', 'Pro', 'Enterprise'])",
},
"groups": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
"features": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
"values": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
},
},
},
},
},
"description": "Feature groups with per-column values",
},
},
},
},
{
Name: "update_landing_features",
Description: "Update the features section with title, description, and optional icon/image for each feature.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"owner", "repo", "features"},
"properties": map[string]any{
"owner": map[string]any{"type": "string", "description": "Repository owner"},
"repo": map[string]any{"type": "string", "description": "Repository name"},
"features": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"title": map[string]any{"type": "string"},
"description": map[string]any{"type": "string"},
"icon": map[string]any{"type": "string"},
"image_url": map[string]any{"type": "string"},
},
},
"description": "Array of features",
},
},
},
},
{
Name: "update_landing_social_proof",
Description: "Update testimonials and client logos for social proof.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"owner", "repo"},
"properties": map[string]any{
"owner": map[string]any{"type": "string", "description": "Repository owner"},
"repo": map[string]any{"type": "string", "description": "Repository name"},
"logos": map[string]any{
"type": "array",
"items": map[string]any{"type": "string"},
"description": "Client/partner logo URLs",
},
"testimonials": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"quote": map[string]any{"type": "string"},
"author": map[string]any{"type": "string"},
"role": map[string]any{"type": "string"},
"avatar": map[string]any{"type": "string"},
},
},
"description": "Testimonials",
},
},
},
},
{
Name: "update_landing_seo",
Description: "Update SEO metadata: title, description, keywords, Open Graph image, Twitter card settings.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"owner", "repo"},
"properties": map[string]any{
"owner": map[string]any{"type": "string", "description": "Repository owner"},
"repo": map[string]any{"type": "string", "description": "Repository name"},
"title": map[string]any{"type": "string", "description": "SEO title"},
"description": map[string]any{"type": "string", "description": "SEO description"},
"keywords": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "SEO keywords"},
"og_image": map[string]any{"type": "string", "description": "Open Graph image URL"},
"twitter_card": map[string]any{"type": "string", "description": "Twitter card type (summary, summary_large_image)"},
"twitter_site": map[string]any{"type": "string", "description": "Twitter @handle"},
},
},
},
{
Name: "update_landing_theme",
Description: "Update the visual theme: primary color, accent color, light/dark/auto mode.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"owner", "repo"},
"properties": map[string]any{
"owner": map[string]any{"type": "string", "description": "Repository owner"},
"repo": map[string]any{"type": "string", "description": "Repository name"},
"primary_color": map[string]any{"type": "string", "description": "Primary brand color (hex, e.g., '#512BD4')"},
"accent_color": map[string]any{"type": "string", "description": "Accent color (hex)"},
"mode": map[string]any{"type": "string", "enum": []string{"light", "dark", "auto"}, "description": "Color mode"},
},
},
},
{
Name: "update_landing_stats",
Description: "Update the stats counters displayed on the landing page. Each stat has a value and label.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"owner", "repo", "stats"},
"properties": map[string]any{
"owner": map[string]any{"type": "string", "description": "Repository owner"},
"repo": map[string]any{"type": "string", "description": "Repository name"},
"stats": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"value": map[string]any{"type": "string"},
"label": map[string]any{"type": "string"},
},
},
"description": "Array of stat counters (e.g., [{value: '15+', label: 'Tools'}])",
},
},
},
},
{
Name: "update_landing_value_props",
Description: "Update the value propositions section. Each value prop has a title, description, and icon.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"owner", "repo", "value_props"},
"properties": map[string]any{
"owner": map[string]any{"type": "string", "description": "Repository owner"},
"repo": map[string]any{"type": "string", "description": "Repository name"},
"value_props": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"title": map[string]any{"type": "string"},
"description": map[string]any{"type": "string"},
"icon": map[string]any{"type": "string"},
},
},
"description": "Array of value propositions",
},
},
},
},
{
Name: "update_landing_cta",
Description: "Update the call-to-action section at the bottom of the page with headline, subheadline, and button.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"owner", "repo"},
"properties": map[string]any{
"owner": map[string]any{"type": "string", "description": "Repository owner"},
"repo": map[string]any{"type": "string", "description": "Repository name"},
"headline": map[string]any{"type": "string", "description": "CTA headline"},
"subheadline": map[string]any{"type": "string", "description": "CTA subheadline"},
"button_label": map[string]any{"type": "string", "description": "Button text"},
"button_url": map[string]any{"type": "string", "description": "Button URL"},
},
},
},
}
// ── Tool Implementations ──────────────────────────────────
func toolGetLandingConfig(ctx *context.APIContext, args map[string]any) (any, error) {
owner, repo, err := resolveOwnerRepo(args)
if err != nil {
return nil, err
}
repoObj, err := getRepoByOwnerAndName(ctx, owner, repo)
if err != nil {
return nil, err
}
config, err := pages_service.GetPagesConfig(ctx, repoObj)
if err != nil {
return nil, fmt.Errorf("get pages config: %w", err)
}
if config == nil {
return map[string]any{"enabled": false, "message": "No landing page configured"}, nil
}
return buildFullResponse(config), nil
}
func toolListLandingTemplates(_ *context.APIContext, _ map[string]any) (any, error) { //nolint:unparam // signature must match tool handler type
templates := pages_module.ValidTemplates()
displayNames := pages_module.TemplateDisplayNames()
result := make([]map[string]string, 0, len(templates))
for _, t := range templates {
result = append(result, map[string]string{
"id": t,
"name": displayNames[t],
})
}
return map[string]any{"templates": result}, nil
}
func toolEnableLandingPage(ctx *context.APIContext, args map[string]any) (any, error) {
owner, repo, err := resolveOwnerRepo(args)
if err != nil {
return nil, err
}
repoObj, err := getRepoByOwnerAndName(ctx, owner, repo)
if err != nil {
return nil, err
}
enabled, _ := args["enabled"].(bool)
template, _ := args["template"].(string)
if enabled {
if template == "" {
template = "open-source-hero"
}
if !pages_module.IsValidTemplate(template) {
return nil, fmt.Errorf("invalid template: %s", template)
}
if err := pages_service.EnablePages(ctx, repoObj, template); err != nil {
return nil, fmt.Errorf("enable pages: %w", err)
}
return map[string]any{"enabled": true, "template": template}, nil
}
if err := pages_service.DisablePages(ctx, repoObj); err != nil {
return nil, fmt.Errorf("disable pages: %w", err)
}
return map[string]any{"enabled": false}, nil
}
func toolUpdateLandingBrand(ctx *context.APIContext, args map[string]any) (any, error) {
config, repoObj, err := getConfigForUpdate(ctx, args)
if err != nil {
return nil, err
}
if v, ok := args["name"].(string); ok {
config.Brand.Name = v
}
if v, ok := args["logo_url"].(string); ok {
config.Brand.LogoURL = v
}
if v, ok := args["tagline"].(string); ok {
config.Brand.Tagline = v
}
if v, ok := args["favicon_url"].(string); ok {
config.Brand.FaviconURL = v
}
return saveAndReturn(ctx, repoObj, config, "brand")
}
func toolUpdateLandingHero(ctx *context.APIContext, args map[string]any) (any, error) {
config, repoObj, err := getConfigForUpdate(ctx, args)
if err != nil {
return nil, err
}
if v, ok := args["headline"].(string); ok {
config.Hero.Headline = v
}
if v, ok := args["subheadline"].(string); ok {
config.Hero.Subheadline = v
}
if v, ok := args["image_url"].(string); ok {
config.Hero.ImageURL = v
}
if v, ok := args["video_url"].(string); ok {
config.Hero.VideoURL = v
}
if v, ok := args["primary_cta_label"].(string); ok {
config.Hero.PrimaryCTA.Label = v
}
if v, ok := args["primary_cta_url"].(string); ok {
config.Hero.PrimaryCTA.URL = v
}
if v, ok := args["secondary_cta_label"].(string); ok {
config.Hero.SecondaryCTA.Label = v
}
if v, ok := args["secondary_cta_url"].(string); ok {
config.Hero.SecondaryCTA.URL = v
}
return saveAndReturn(ctx, repoObj, config, "hero")
}
func toolUpdateLandingPricing(ctx *context.APIContext, args map[string]any) (any, error) {
config, repoObj, err := getConfigForUpdate(ctx, args)
if err != nil {
return nil, err
}
if v, ok := args["headline"].(string); ok {
config.Pricing.Headline = v
}
if v, ok := args["subheadline"].(string); ok {
config.Pricing.Subheadline = v
}
if plans, ok := args["plans"].([]any); ok {
config.Pricing.Plans = nil
for _, p := range plans {
if pm, ok := p.(map[string]any); ok {
plan := pages_module.PricingPlanConfig{
Name: strVal(pm, "name"),
Price: strVal(pm, "price"),
Period: strVal(pm, "period"),
CTA: strVal(pm, "cta"),
Featured: boolVal(pm, "featured"),
}
if features, ok := pm["features"].([]any); ok {
for _, f := range features {
if s, ok := f.(string); ok {
plan.Features = append(plan.Features, s)
}
}
}
config.Pricing.Plans = append(config.Pricing.Plans, plan)
}
}
}
return saveAndReturn(ctx, repoObj, config, "pricing")
}
func toolUpdateLandingComparison(ctx *context.APIContext, args map[string]any) (any, error) {
config, repoObj, err := getConfigForUpdate(ctx, args)
if err != nil {
return nil, err
}
if v, ok := args["enabled"].(bool); ok {
config.Comparison.Enabled = v
}
if v, ok := args["headline"].(string); ok {
config.Comparison.Headline = v
}
if v, ok := args["subheadline"].(string); ok {
config.Comparison.Subheadline = v
}
if cols, ok := args["columns"].([]any); ok {
config.Comparison.Columns = nil
for _, c := range cols {
if s, ok := c.(string); ok {
config.Comparison.Columns = append(config.Comparison.Columns, pages_module.ComparisonColumnConfig{Name: s})
} else if cm, ok := c.(map[string]any); ok {
config.Comparison.Columns = append(config.Comparison.Columns, pages_module.ComparisonColumnConfig{
Name: strVal(cm, "name"),
Highlight: boolVal(cm, "highlight"),
})
}
}
}
if groups, ok := args["groups"].([]any); ok {
config.Comparison.Groups = nil
for _, g := range groups {
if gm, ok := g.(map[string]any); ok {
group := pages_module.ComparisonGroupConfig{Name: strVal(gm, "name")}
if features, ok := gm["features"].([]any); ok {
for _, f := range features {
if fm, ok := f.(map[string]any); ok {
feature := pages_module.ComparisonFeatureConfig{Name: strVal(fm, "name")}
if vals, ok := fm["values"].([]any); ok {
for _, v := range vals {
if s, ok := v.(string); ok {
feature.Values = append(feature.Values, s)
}
}
}
group.Features = append(group.Features, feature)
}
}
}
config.Comparison.Groups = append(config.Comparison.Groups, group)
}
}
}
return saveAndReturn(ctx, repoObj, config, "comparison")
}
func toolUpdateLandingFeatures(ctx *context.APIContext, args map[string]any) (any, error) {
config, repoObj, err := getConfigForUpdate(ctx, args)
if err != nil {
return nil, err
}
if features, ok := args["features"].([]any); ok {
config.Features = nil
for _, f := range features {
if fm, ok := f.(map[string]any); ok {
config.Features = append(config.Features, pages_module.FeatureConfig{
Title: strVal(fm, "title"),
Description: strVal(fm, "description"),
Icon: strVal(fm, "icon"),
ImageURL: strVal(fm, "image_url"),
})
}
}
}
return saveAndReturn(ctx, repoObj, config, "features")
}
func toolUpdateLandingSocialProof(ctx *context.APIContext, args map[string]any) (any, error) {
config, repoObj, err := getConfigForUpdate(ctx, args)
if err != nil {
return nil, err
}
if logos, ok := args["logos"].([]any); ok {
config.SocialProof.Logos = nil
for _, l := range logos {
if s, ok := l.(string); ok {
config.SocialProof.Logos = append(config.SocialProof.Logos, s)
}
}
}
if testimonials, ok := args["testimonials"].([]any); ok {
config.SocialProof.Testimonials = nil
for _, t := range testimonials {
if tm, ok := t.(map[string]any); ok {
config.SocialProof.Testimonials = append(config.SocialProof.Testimonials, pages_module.TestimonialConfig{
Quote: strVal(tm, "quote"),
Author: strVal(tm, "author"),
Role: strVal(tm, "role"),
Avatar: strVal(tm, "avatar"),
})
}
}
}
return saveAndReturn(ctx, repoObj, config, "social_proof")
}
func toolUpdateLandingSEO(ctx *context.APIContext, args map[string]any) (any, error) {
config, repoObj, err := getConfigForUpdate(ctx, args)
if err != nil {
return nil, err
}
if v, ok := args["title"].(string); ok {
config.SEO.Title = v
}
if v, ok := args["description"].(string); ok {
config.SEO.Description = v
}
if v, ok := args["og_image"].(string); ok {
config.SEO.OGImage = v
}
if v, ok := args["twitter_card"].(string); ok {
config.SEO.TwitterCard = v
}
if v, ok := args["twitter_site"].(string); ok {
config.SEO.TwitterSite = v
}
if keywords, ok := args["keywords"].([]any); ok {
config.SEO.Keywords = nil
for _, k := range keywords {
if s, ok := k.(string); ok {
config.SEO.Keywords = append(config.SEO.Keywords, s)
}
}
}
return saveAndReturn(ctx, repoObj, config, "seo")
}
func toolUpdateLandingTheme(ctx *context.APIContext, args map[string]any) (any, error) {
config, repoObj, err := getConfigForUpdate(ctx, args)
if err != nil {
return nil, err
}
if v, ok := args["primary_color"].(string); ok {
config.Theme.PrimaryColor = v
}
if v, ok := args["accent_color"].(string); ok {
config.Theme.AccentColor = v
}
if v, ok := args["mode"].(string); ok {
config.Theme.Mode = v
}
return saveAndReturn(ctx, repoObj, config, "theme")
}
func toolUpdateLandingStats(ctx *context.APIContext, args map[string]any) (any, error) {
config, repoObj, err := getConfigForUpdate(ctx, args)
if err != nil {
return nil, err
}
if stats, ok := args["stats"].([]any); ok {
config.Stats = nil
for _, s := range stats {
if sm, ok := s.(map[string]any); ok {
config.Stats = append(config.Stats, pages_module.StatConfig{
Value: strVal(sm, "value"),
Label: strVal(sm, "label"),
})
}
}
}
return saveAndReturn(ctx, repoObj, config, "stats")
}
func toolUpdateLandingValueProps(ctx *context.APIContext, args map[string]any) (any, error) {
config, repoObj, err := getConfigForUpdate(ctx, args)
if err != nil {
return nil, err
}
if vps, ok := args["value_props"].([]any); ok {
config.ValueProps = nil
for _, v := range vps {
if vm, ok := v.(map[string]any); ok {
config.ValueProps = append(config.ValueProps, pages_module.ValuePropConfig{
Title: strVal(vm, "title"),
Description: strVal(vm, "description"),
Icon: strVal(vm, "icon"),
})
}
}
}
return saveAndReturn(ctx, repoObj, config, "value_props")
}
func toolUpdateLandingCTA(ctx *context.APIContext, args map[string]any) (any, error) {
config, repoObj, err := getConfigForUpdate(ctx, args)
if err != nil {
return nil, err
}
if v, ok := args["headline"].(string); ok {
config.CTASection.Headline = v
}
if v, ok := args["subheadline"].(string); ok {
config.CTASection.Subheadline = v
}
if v, ok := args["button_label"].(string); ok {
config.CTASection.Button.Label = v
}
if v, ok := args["button_url"].(string); ok {
config.CTASection.Button.URL = v
}
return saveAndReturn(ctx, repoObj, config, "cta_section")
}
// ── Helpers ──────────────────────────────────
func getConfigForUpdate(ctx *context.APIContext, args map[string]any) (*pages_module.LandingConfig, *repo_model.Repository, error) {
owner, repo, err := resolveOwnerRepo(args)
if err != nil {
return nil, nil, err
}
repoObj, err := getRepoByOwnerAndName(ctx, owner, repo)
if err != nil {
return nil, nil, err
}
config, err := pages_service.GetPagesConfig(ctx, repoObj)
if err != nil {
return nil, nil, fmt.Errorf("get config: %w", err)
}
if config == nil {
config = pages_module.DefaultConfig()
}
return config, repoObj, nil
}
func saveAndReturn(ctx *context.APIContext, repo *repo_model.Repository, config *pages_module.LandingConfig, section string) (any, error) {
configJSON, _ := json.Marshal(config)
hash := pages_module.HashConfig(configJSON)
existing, _ := repo_model.GetPagesConfigByRepoID(ctx, repo.ID)
if existing != nil {
existing.ConfigJSON = string(configJSON)
existing.ConfigHash = hash
existing.Template = repo_model.PagesTemplate(config.Template)
existing.Enabled = config.Enabled
if err := repo_model.UpdatePagesConfig(ctx, existing); err != nil {
return nil, fmt.Errorf("save config: %w", err)
}
} else {
if err := repo_model.CreatePagesConfig(ctx, &repo_model.PagesConfig{
RepoID: repo.ID,
Enabled: config.Enabled,
Template: repo_model.PagesTemplate(config.Template),
ConfigJSON: string(configJSON),
ConfigHash: hash,
}); err != nil {
return nil, fmt.Errorf("create config: %w", err)
}
}
return map[string]any{
"success": true,
"section": section,
"message": fmt.Sprintf("Updated %s section", section),
}, nil
}
func strVal(m map[string]any, key string) string {
if v, ok := m[key].(string); ok {
return v
}
return ""
}
func boolVal(m map[string]any, key string) bool {
if v, ok := m[key].(bool); ok {
return v
}
return false
}
func resolveOwnerRepo(args map[string]any) (string, string, error) {
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
if owner == "" || repo == "" {
return "", "", errors.New("owner and repo are required")
}
return owner, repo, nil
}
func getRepoByOwnerAndName(ctx *context.APIContext, owner, repo string) (*repo_model.Repository, error) {
ownerObj, err := user_model.GetUserByName(ctx, owner)
if err != nil {
return nil, fmt.Errorf("owner not found: %s", owner)
}
repoObj, err := repo_model.GetRepositoryByName(ctx, ownerObj.ID, repo)
if err != nil {
return nil, fmt.Errorf("repo not found: %s/%s", owner, repo)
}
return repoObj, nil
}

View File

@@ -148,6 +148,7 @@ func CheckAppUpdate(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
latestRelease.Repo = repo
// Find the appropriate asset for this platform/arch
downloadURL, platformInfo := findUpdateAsset(latestRelease, platform, arch)
@@ -346,6 +347,7 @@ func ListReleasesV2(ctx *context.APIContext) {
// Convert to API format
apiReleases := make([]*api.Release, 0, len(releases))
for _, release := range releases {
release.Repo = repo
apiReleases = append(apiReleases, convertToAPIRelease(repo, release))
}
@@ -388,6 +390,7 @@ func GetReleaseV2(ctx *context.APIContext) {
return
}
release.Repo = repo
ctx.JSON(http.StatusOK, convertToAPIRelease(repo, release))
}
@@ -436,6 +439,7 @@ func GetLatestReleaseV2(ctx *context.APIContext) {
return
}
release.Repo = repo
ctx.JSON(http.StatusOK, convertToAPIRelease(repo, release))
}

View File

@@ -1533,10 +1533,6 @@ func PagesAdvancedPost(ctx *context.Context) {
// Parse remaining fields
config.Advanced.CustomCSS = ctx.FormString("custom_css")
config.Advanced.CustomHead = ctx.FormString("custom_head")
config.Advanced.GooglePlayID = ctx.FormString("google_play_id")
config.Advanced.AppStoreID = ctx.FormString("app_store_id")
config.Advanced.PublicReleases = ctx.FormBool("public_releases")
config.Advanced.HideMobileReleases = ctx.FormBool("hide_mobile_releases")
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)

View File

@@ -53,7 +53,7 @@
</a>
{{end}}
{{end}}
<details class="item toggleable-item" {{if or .PageIsSettingsPagesGeneral .PageIsSettingsPagesBrand .PageIsSettingsPagesHero .PageIsSettingsPagesContent .PageIsSettingsPagesComparison .PageIsSettingsPagesSocial .PageIsSettingsPagesPricing .PageIsSettingsPagesFooter .PageIsSettingsPagesTheme .PageIsSettingsPagesLanguages}}open{{end}}>
<details class="item toggleable-item" {{if or .PageIsSettingsPagesGeneral .PageIsSettingsPagesBrand .PageIsSettingsPagesHero .PageIsSettingsPagesContent .PageIsSettingsPagesComparison .PageIsSettingsPagesSocial .PageIsSettingsPagesPricing .PageIsSettingsPagesFooter .PageIsSettingsPagesTheme .PageIsSettingsPagesLanguages .PageIsSettingsPagesAdvanced}}open{{end}}>
<summary>{{ctx.Locale.Tr "repo.settings.pages"}}</summary>
<div class="menu">
<a class="{{if .PageIsSettingsPagesGeneral}}active {{end}}item" href="{{.RepoLink}}/settings/pages">
@@ -86,6 +86,9 @@
<a class="{{if .PageIsSettingsPagesLanguages}}active {{end}}item" href="{{.RepoLink}}/settings/pages/languages">
{{ctx.Locale.Tr "repo.settings.pages.languages"}}
</a>
<a class="{{if .PageIsSettingsPagesAdvanced}}active {{end}}item" href="{{.RepoLink}}/settings/pages/advanced">
{{ctx.Locale.Tr "repo.settings.pages.advanced"}}
</a>
</div>
</details>
<details class="item toggleable-item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables .PageIsActionsSettingsGeneral}}open{{end}}>

View File

@@ -57,33 +57,6 @@
<textarea name="custom_head" rows="4" placeholder="<meta ...>">{{.Config.Advanced.CustomHead}}</textarea>
</div>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.app_stores"}}</h5>
<div class="two fields">
<div class="field">
<label>Google Play ID</label>
<input name="google_play_id" value="{{.Config.Advanced.GooglePlayID}}" placeholder="com.example.app">
</div>
<div class="field">
<label>App Store ID</label>
<input name="app_store_id" value="{{.Config.Advanced.AppStoreID}}" placeholder="123456789">
</div>
</div>
<div class="inline fields">
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="public_releases" {{if .Config.Advanced.PublicReleases}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pages.public_releases"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="hide_mobile_releases" {{if .Config.Advanced.HideMobileReleases}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pages.hide_mobile_releases"}}</label>
</div>
</div>
</div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</div>

View File

@@ -1,37 +0,0 @@
{{if .PagesEnabled}}
<div class="ui secondary pointing menu tw-mb-4">
<a class="{{if .PageIsSettingsPagesGeneral}}active {{end}}item" href="{{.RepoLink}}/settings/pages">
{{ctx.Locale.Tr "repo.settings.pages.general"}}
</a>
<a class="{{if .PageIsSettingsPagesBrand}}active {{end}}item" href="{{.RepoLink}}/settings/pages/brand">
{{ctx.Locale.Tr "repo.settings.pages.brand"}}
</a>
<a class="{{if .PageIsSettingsPagesHero}}active {{end}}item" href="{{.RepoLink}}/settings/pages/hero">
{{ctx.Locale.Tr "repo.settings.pages.hero"}}
</a>
<a class="{{if .PageIsSettingsPagesContent}}active {{end}}item" href="{{.RepoLink}}/settings/pages/content">
{{ctx.Locale.Tr "repo.settings.pages.content"}}
</a>
<a class="{{if .PageIsSettingsPagesComparison}}active {{end}}item" href="{{.RepoLink}}/settings/pages/comparison">
{{ctx.Locale.Tr "repo.settings.pages.comparison"}}
</a>
<a class="{{if .PageIsSettingsPagesSocial}}active {{end}}item" href="{{.RepoLink}}/settings/pages/social">
{{ctx.Locale.Tr "repo.settings.pages.social"}}
</a>
<a class="{{if .PageIsSettingsPagesPricing}}active {{end}}item" href="{{.RepoLink}}/settings/pages/pricing">
{{ctx.Locale.Tr "repo.settings.pages.pricing"}}
</a>
<a class="{{if .PageIsSettingsPagesFooter}}active {{end}}item" href="{{.RepoLink}}/settings/pages/footer">
{{ctx.Locale.Tr "repo.settings.pages.footer"}}
</a>
<a class="{{if .PageIsSettingsPagesTheme}}active {{end}}item" href="{{.RepoLink}}/settings/pages/theme">
{{ctx.Locale.Tr "repo.settings.pages.theme"}}
</a>
<a class="{{if .PageIsSettingsPagesLanguages}}active {{end}}item" href="{{.RepoLink}}/settings/pages/languages">
{{ctx.Locale.Tr "repo.settings.pages.languages"}}
</a>
<a class="{{if .PageIsSettingsPagesAdvanced}}active {{end}}item" href="{{.RepoLink}}/settings/pages/advanced">
{{ctx.Locale.Tr "repo.settings.pages.advanced"}}
</a>
</div>
{{end}}