2
0

55 Commits

Author SHA1 Message Date
416278c747 fix(pages): allow cross-promotion to private repos with pages
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m54s
Build and Release / Unit Tests (push) Successful in 5m29s
Build and Release / Lint (push) Successful in 7m53s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m43s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 5m3s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m41s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m26s
Build and Release / Build Binary (linux/arm64) (push) Failing after 15m15s
Remove the private repo filter from cross-promotion targets. Landing pages are publicly accessible regardless of the repository's private flag, so private repos with pages enabled should be allowed as cross-promotion targets.
2026-04-24 23:26:16 -04:00
58cf5dd410 fix(pages): strip AI placeholder values from translations
Filter out placeholder strings like "<UNKNOWN>", "<EMPTY>", and "N/A" that AI sometimes returns for empty fields. Only include non-empty fields in the translatable content to prevent placeholders from being generated and saved in translation overlays.
2026-04-24 23:24:01 -04:00
0ab62c2b95 feat(pages): add customizable headlines for value props and features
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m3s
Build and Release / Unit Tests (push) Successful in 11m1s
Build and Release / Lint (push) Successful in 11m18s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m1s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 5m17s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m7s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m36s
Build and Release / Build Binary (linux/arm64) (push) Successful in 10m23s
Add headline and subheadline fields to value propositions and features sections in landing page configuration. This allows users to customize the large section headings independently from the small section labels.
2026-04-24 22:20:38 -04:00
97b534f66b build(ci): replace go mod tidy with go mod download
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m6s
Build and Release / Unit Tests (push) Successful in 11m6s
Build and Release / Lint (push) Successful in 11m18s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m3s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h6m29s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m26s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m45s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 11m23s
Use go mod download instead of go mod tidy in CI workflows to avoid unnecessary modifications to go.mod and go.sum files during builds
2026-04-24 02:44:29 -04:00
eb969b8f82 build(deps): update go toolchain from 1.25.5 to 1.25.9
Some checks failed
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m14s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 6m32s
Build and Release / Lint (push) Successful in 6m44s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m57s
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 / Build Binaries (amd64, windows, windows-latest) (push) Has been cancelled
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
2026-04-24 02:27:54 -04:00
b310e8ed18 build(deps): exclude gitea SDK v0.24.0 to pin to v0.22.0
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m4s
Build and Release / Unit Tests (push) Successful in 10m39s
Build and Release / Lint (push) Successful in 11m10s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m38s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m1s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m4s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m53s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m58s
Add exclusion for gitea SDK v0.24.0 in addition to v0.24.1 since the entire v0.24.x series requires go 1.26
2026-04-24 01:46:14 -04:00
e8286de3be fix(deps): exclude gitea SDK v0.24.1 requiring Go 1.26
Some checks failed
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m45s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 6m13s
Build and Release / Lint (push) Successful in 6m24s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m38s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m59s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m46s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m49s
Build and Release / Build Binary (linux/arm64) (push) Failing after 18m45s
Add exclude directive for code.gitea.io/sdk/gitea v0.24.1 to prevent automatic upgrade to version requiring Go 1.26 while project remains on Go 1.25.
2026-04-24 00:54:06 -04:00
4648a021c5 Update build.yml
Some checks failed
Build and Release / Build Binaries (amd64, windows, windows-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
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m21s
Build and Release / Unit Tests (push) Successful in 5m10s
Build and Release / Lint (push) Successful in 7m40s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m4s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m40s
2026-04-23 18:08:20 -04:00
1a335e741b fix(ci): download modules before bindata generation
Some checks failed
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m12s
Build and Release / Create Release (push) Successful in 1s
Build and Release / Unit Tests (push) Successful in 10m25s
Build and Release / Lint (push) Successful in 11m51s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 3m10s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m28s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m44s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m26s
Build and Release / Build Binary (linux/arm64) (push) Successful in 9m13s
Add explicit go mod download before make generate to ensure all dependencies (including updated vault) are available for bindata generation.
2026-04-23 17:04:06 -04:00
998337e80d fix(ci): revert to go mod tidy with error suppression
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m50s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m26s
Build and Release / Lint (push) Successful in 10m32s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 3m5s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m55s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m46s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Revert back to go mod tidy but add || true to prevent build failures from tidy errors after vault dependency update.
2026-04-23 16:45:42 -04:00
009566a3ce fix(ci): use go mod download instead of tidy for vault
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m23s
Build and Release / Unit Tests (push) Successful in 10m43s
Build and Release / Lint (push) Successful in 11m47s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 2m26s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 3m44s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 2m17s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Failing after 9h4m5s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Replace go mod tidy with go mod download after updating vault dependency to avoid modifying go.sum unnecessarily and speed up the build.
2026-04-23 16:26:12 -04:00
2ca88587d4 chore(ci): upgrade to Go 1.26.2 and remove verbose flag
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m51s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m9s
Build and Release / Lint (push) Successful in 10m43s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 2m9s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m36s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m54s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m22s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Update Go version to 1.26.2 and remove -v flag from go mod tidy commands to reduce log noise.
2026-04-23 16:04:47 -04:00
05f8df8a9e Update build.yml
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m9s
Build and Release / Unit Tests (push) Successful in 11m12s
Build and Release / Lint (push) Successful in 11m36s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m47s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m25s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m55s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m27s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
2026-04-23 15:18:10 -04:00
44b6c62093 refactor(ci): move GOPROXY/GOPRIVATE to inline exports
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m20s
Build and Release / Unit Tests (push) Successful in 10m30s
Build and Release / Lint (push) Successful in 11m44s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 2m6s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m42s
Build and Release / Build Binary (linux/arm64) (push) Failing after 11m27s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 11m32s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 11m37s
Move environment variable overrides from step-level env to inline exports within run scripts for better visibility and consistency across Unix/Windows steps.
2026-04-23 11:33:41 -04:00
4557edfe98 fix(ci): use GOPROXY=direct for vault dependency updates
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m45s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 10m4s
Build and Release / Lint (push) Successful in 10m51s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 2m16s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m7s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h6m58s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m55s
Build and Release / Build Binary (linux/arm64) (push) Successful in 11m44s
Replace git URL rewriting with GOPROXY=direct and GOPRIVATE="" to fetch vault dependency directly from source without proxy, avoiding authentication issues.
2026-04-23 10:55:41 -04:00
3b8cfc0c03 fix(ci): add git auth for vault dependency updates
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m50s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m18s
Build and Release / Lint (push) Successful in 10m30s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m36s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m36s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m11s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Configure git URL rewriting with RELEASE_TOKEN for vault dependency steps to ensure authenticated access to private repository when updating go.mod.
2026-04-23 10:33:09 -04:00
55568524a7 fix(ci): auto-append .0 to Go version for download URL
Some checks failed
Build and Release / Unit Tests (push) Successful in 3m52s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m28s
Build and Release / Lint (push) Successful in 10m43s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m41s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m5s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 11m32s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m21s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Add logic to append .0 patch version when GO_VERSION is in major.minor format (e.g., 1.26 → 1.26.0) since go.dev download URLs require full semver.

Also remove redundant GOPROXY=direct and GOPRIVATE="" overrides in vault dependency steps since these are already set globally.
2026-04-23 10:01:36 -04:00
62f4a3ce37 fix(ci): use Go 1.26 instead of 1.26.2
Some checks failed
Build and Release / Unit Tests (push) Successful in 4m3s
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m38s
Build and Release / Lint (push) Successful in 10m45s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 3m26s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 5m53s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h9m9s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m56s
Build and Release / Build Binary (linux/arm64) (push) Failing after 16s
Use major.minor version format to automatically pick up latest patch releases.
2026-04-23 02:28:43 -04:00
3c8405a3b2 chore(ci): upgrade Go version to 1.26.2 2026-04-23 02:27:10 -04:00
e105e047a4 feat(mirror): add option to exclude hidden files from push mirrors
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m50s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 9m30s
Build and Release / Lint (push) Successful in 10m36s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m44s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h7m26s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m9s
Build and Release / Build Binary (linux/arm64) (push) Failing after 17m34s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 1m34s
Add ExcludeHiddenFiles option to push mirrors to filter out dotfiles and other hidden paths:
- Add exclude_hidden_files column to push_mirror table
- Implement tree filtering using git plumbing (ls-tree, mktree, commit-tree)
- Add UI toggle in repository mirror settings
- Update locale files for all languages

Migration: v370 adds exclude_hidden_files column with default false.

This allows users to mirror repositories while excluding .git, .env, and other sensitive dotfiles from the pushed copy.
2026-04-22 22:30:17 -04:00
f9f2d45c13 feat(pages): add cross-promote translation support
Adds translation support for cross-promote section in landing pages. Users can now provide localized versions of cross-promote headline, subheadline, and navigation label.

Updates TranslationView with CrossPromoteHeadline, CrossPromoteSubheadline, and NavLabelCrossPromote fields. Updates translation parsing and JSON building to handle cross_promote overlay.

Updates buildTranslatableContent to include cross-promote fields in generated translation template with "Related Offerings" default headline.

Completes cross-promote feature by enabling full i18n support alongside the section configuration added in previous commit.
2026-04-19 21:45:30 -04:00
3a62a5d8c1 feat(pages): add cross-promote section to landing pages
Adds CrossPromoteSectionConfig to landing page configuration, allowing repositories to showcase related projects/products. Section includes customizable headline and subheadline.

Adds LabelCrossPromote to NavigationConfig for translatable section heading. All templates default to "Related Offerings" with template-specific variations (e.g., "Related Offerings" for architecture-deep-dive).

Updates all 9 landing page templates to render cross-promote section when enabled. Adds locale strings for cross-promote UI across all 30 supported languages.

Useful for organizations promoting a product family or related open-source projects from a single landing page.
2026-04-19 21:37:42 -04:00
f2f4367dbe fix(i18n): fix character encoding in locale files
All checks were successful
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 4m1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m28s
Build and Release / Lint (push) Successful in 6m12s
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
Fixes mojibake (garbled text) in 28 locale files caused by incorrect UTF-8 encoding. Replaces double-encoded characters with proper Unicode representations.

Examples:
- German: "Öffentliche" → "Öffentliche"
- Spanish: "reproducción" → "reproducción"
- French: "dépôts" → "dépôts"
- Czech: "ZaÄíná" → "Začíná"
- Finnish: "että" → "että"

Affects cs-CZ, de-DE, el-GR, es-ES, fa-IR, fi-FI, fr-FR, ga-IE, hi-IN, hu-HU, id-ID, is-IS, it-IT, ja-JP, ko-KR, lv-LV, nl-NL, pl-PL, pt-BR, pt-PT, ro-RO, ru-RU, si-LK, sk-SK, sv-SE, tr-TR, uk-UA, zh-CN.

No functional changes - purely encoding correction for proper display of non-ASCII characters.
2026-04-19 20:53:48 -04:00
7b4e85a473 feat(pages): add customizable section labels for landing pages
Adds ability to customize section headings on repository landing pages. Users can now override default labels for value propositions and features sections.

New fields:
- LabelValueProps: Custom heading for value propositions section (default: "Why choose us")
- LabelFeatures: Custom heading for features section (default: "Capabilities")

Useful for branding consistency or localization when default English headings don't fit the project's tone or language.
2026-04-19 20:31:22 -04:00
d8904e2846 fix(ui): add missing labels to landing page URL inputs
Adds missing label elements to logo URL, favicon URL, and hero image URL input fields in landing page settings. Improves form accessibility and visual consistency with other form fields.
2026-04-19 18:29:36 -04:00
4fabef6a65 feat(api): add organization management API v2 and MCP tools
All checks were successful
Build and Release / Build Binaries (amd64, windows, windows-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
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 3m59s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 8m32s
Build and Release / Lint (push) Successful in 9m40s
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
Adds comprehensive v2 API endpoints for organization management:
- GET /api/v2/user/orgs - list user's organizations
- GET /api/v2/orgs/{org}/overview - org overview with pinned repos, members, stats, profile, recent activity
- GET /api/v2/orgs/{org}/repos - list org repositories with grouping support
- GET /api/v2/orgs/{org}/profile/readme - get org profile README
- PATCH /api/v2/orgs/{org} - update org metadata (requires owner/admin)
- PUT /api/v2/orgs/{org}/profile/readme - update profile README
- POST /api/v2/orgs/{org}/pinned - pin repository
- DELETE /api/v2/orgs/{org}/pinned/{repo} - unpin repository

Adds 8 MCP tools for AI assistant access: list_orgs, get_org_overview, update_org, list_org_repos, get_org_profile_readme, update_org_profile_readme, pin_org_repo, unpin_org_repo.

Introduces OrgOverviewV2 and OrgRecentActivity structs. Adds org/profile service for README and pinning operations.
2026-04-19 17:32:06 -04:00
f26bd3e273 fix(explore): prevent group splitting across pages in org explorer
When organization grouping is enabled, modifies sort order to group by group_header first before applying user-selected ordering. This prevents organizations in the same group from being split across pagination boundaries.

Adds CASE expression to sort ungrouped orgs (null/empty group_header) last, then groups alphabetically, then applies the requested orderBy within each group.
2026-04-19 17:16:14 -04:00
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
242ebf2dc1 chore(ci): bump Go version to 1.25.5
Some checks failed
Build and Release / Unit Tests (push) Successful in 6m13s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m1s
Build and Release / Lint (push) Successful in 7m20s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 3m20s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 3m19s
Build and Release / Build Binary (linux/arm64) (push) Failing after 2m22s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h10m55s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 13m34s
2026-03-30 02:28:27 -04:00
46f7570d25 feat(pages): add static route configuration for direct file serving
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Failing after 47s
Build and Release / Integration Tests (PostgreSQL) (push) Failing after 1m29s
Build and Release / Lint (push) Failing after 2m16s
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
Allow repository pages to serve files directly at specific URL paths instead of rendering the landing page. Supports exact paths (/badge.svg) and glob patterns (/schema/*). Add advanced settings UI and API endpoints for managing static routes alongside existing redirects and custom code options.
2026-03-30 02:07:41 -04:00
48aab974fe fix(pages): populate defaults before translation overlay
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m48s
Build and Release / Unit Tests (push) Successful in 8m44s
Build and Release / Lint (push) Successful in 9m14s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 0s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m49s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m52s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m56s
Build and Release / Build Binary (linux/arm64) (push) Failing after 17m26s
Add ensureTemplateDefaults function that fills navigation labels and section headlines with template-specific defaults before applying translation overlay. Ensures these fields are present in base config JSON so translations can override them via deep-merge. Fixes issue where translated labels wouldn't apply because base config had empty strings instead of default values. Prevents templates from falling back to hardcoded English text.
2026-03-18 00:05:05 -04:00
965ef8966f feat(pages): add navigation label translation support
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m4s
Build and Release / Unit Tests (push) Successful in 4m38s
Build and Release / Lint (push) Successful in 6m26s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m52s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m52s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m22s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m38s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m46s
Add translation support for navigation section labels (Value Props, Features, Pricing, Blog, Gallery, Compare, etc.). Adds TemplateDefaultLabels function that returns template-specific creative names (e.g., "Systems Analysis" for value props in Architecture Deep Dive). Auto-applies defaults when enabling pages or changing templates. Includes UI fields in languages settings and translation JSON serialization. Enables full localization of section headings.
2026-03-17 23:34:29 -04:00
17028589c8 fix(i18n): use native language names in display map
Some checks failed
Build and Release / Create Release (push) Has been skipped
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 / Integration Tests (PostgreSQL) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Lint (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Unit Tests (push) Has been cancelled
Replace English language names with native names for better UX (e.g., "日本語" instead of "Japanese", "Español" instead of "Espanol"). Adds proper diacritics and uses native scripts. Makes language selection more intuitive for non-English speakers.
2026-03-17 21:50:15 -04:00
80096bfbf9 feat(ui): add loading indicators for AI generation actions
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m24s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m26s
Build and Release / Lint (push) Successful in 8m44s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 0s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m39s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m53s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m28s
Build and Release / Build Binary (linux/arm64) (push) Failing after 12m10s
Show loading spinner and message when AI content generation or translation is in progress. Disables submit button and hides form to prevent duplicate submissions. Adds Copilot icon to AI buttons. Marks file inputs with data-ays-ignore to prevent "unsaved changes" warnings. Improves UX by providing visual feedback during long-running AI operations.
2026-03-17 21:42:39 -04:00
22844f6437 fix(pages): use navigation labels for section headings and add defaults
Use LabelValueProps and LabelFeatures from navigation config for section headings instead of hardcoded text. Removes hardcoded subheadlines that don't translate well. Adds default headlines for blog ("Latest Posts"), gallery ("Gallery"), and comparison ("How We Compare") sections when not configured. Improves consistency and translation support across all 8 page templates.
2026-03-17 21:34:34 -04:00
737e323fcb perf(pages): parallelize bulk AI translation
Run AI translations for multiple languages concurrently using goroutines and sync.WaitGroup. Protects shared counters with mutex. Significantly reduces total translation time when translating many languages. For example, translating 10 languages now takes ~10 seconds instead of ~100 seconds (assuming 10s per language).
2026-03-17 21:24:05 -04:00
00024298d0 feat(pages): add JSON struct tags to pages config types
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
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
b27dd0cda8 refactor(pages): use maps.Copy in deepMergeArrays
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Unit Tests (push) Successful in 3m22s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m53s
Build and Release / Lint (push) Successful in 9m1s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 0s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m53s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 5m14s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m32s
Build and Release / Build Binary (linux/arm64) (push) Failing after 14m38s
Replace manual map copy loop with maps.Copy from Go 1.21 stdlib. Cleaner and potentially more efficient. Pre-allocates merged map with correct capacity.
2026-03-17 11:55:18 -04:00
43e490d933 feat(i18n): add bulk AI translation for all languages
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m21s
Build and Release / Lint (push) Failing after 8m46s
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 / Unit Tests (push) Successful in 8m49s
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 "Translate All (AI)" button to pages language settings that translates all configured languages in one operation. Shows success/partial success messages with counts. Adds locale keys for all 29 languages. Also removes trailing newlines from locale files for consistency.
2026-03-17 11:35:20 -04:00
d02f25c0ba fix(i18n): remove BOM from locale files
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m7s
Build and Release / Unit Tests (push) Successful in 4m38s
Build and Release / Lint (push) Successful in 6m35s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m59s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m55s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m19s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m58s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m32s
Remove UTF-8 BOM (byte order mark) from 14 locale JSON files. The BOM character (U+FEFF) at the start of files can cause parsing issues in some JSON parsers and is not needed for UTF-8 files. Affects ko-KR, lv-LV, nl-NL, pl-PL, pt-BR, pt-PT, ru-RU, si-LK, sk-SK, sv-SE, tr-TR, uk-UA, zh-CN, zh-TW.
2026-03-17 09:01:01 -04:00
12341079e1 feat(i18n): add translation keys for new pages sections
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m41s
Build and Release / Unit Tests (push) Successful in 8m55s
Build and Release / Lint (push) Successful in 9m23s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 1m4s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Failing after 9h0m35s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 50s
Build and Release / Build Binary (linux/arm64) (push) Failing after 32s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 54s
Add locale keys for translating new landing page sections: gallery, comparison, blog, and expanded fields for stats, pricing, testimonials, and footer. Adds section headers and field labels for translation UI. Includes keys for all 29 supported languages. Enables full localization of new pages features added in recent commits.
2026-03-17 03:52:30 -04:00
c5e35e3466 refactor(pages): move app store badges to hero section and add section IDs
Move Google Play and App Store badges from downloads section to hero section for better visibility. Removes redundant display when both downloads and app stores are present. Add id attributes to major sections (hero, stats, downloads, social-proof, cta, etc.) for anchor link navigation. Applies to all 9 page templates. Improves UX for mobile app landing pages.
2026-03-17 03:25:14 -04:00
c0fcf16794 style(api): sort imports in pages API
All checks were successful
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m44s
Build and Release / Unit Tests (push) Successful in 8m45s
Build and Release / Lint (push) Successful in 9m18s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m53s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m46s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m13s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m45s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m38s
2026-03-17 01:27:51 -04:00
c5786aab2b fix(api): use internal json module in pages API
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m48s
Build and Release / Lint (push) Failing after 8m37s
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 Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 8m53s
Replace stdlib encoding/json with internal json module for consistency with rest of codebase. Ensures proper handling of edge cases and custom marshaling behavior.
2026-03-17 01:15:01 -04:00
fe5b504b97 feat(api): add v2 API endpoints for landing page configuration
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.
2026-03-17 00:52:18 -04:00
a1f477a381 fix(ui): add missing pages settings nav items and cleanup labels
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m53s
Build and Release / Unit Tests (push) Successful in 8m43s
Build and Release / Lint (push) Successful in 9m20s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m30s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m44s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m59s
Build and Release / Build Binary (linux/arm64) (push) Failing after 24m13s
Add Comparison and Languages items to pages settings sidebar navigation. Fix sidebar not staying open when on those pages. Remove redundant labels from URL input fields that appear after "or use a URL" dividers (logo, favicon, hero image). Improves navigation consistency and UI clarity.
2026-03-17 00:14:48 -04:00
77 changed files with 65898 additions and 58609 deletions

View File

@@ -16,7 +16,7 @@ env:
GOPRIVATE: git.marketally.com
GONOSUMDB: git.marketally.com
GOTOOLCHAIN: local
GO_VERSION: "1.25.0"
GO_VERSION: "1.25.9"
NODE_VERSION: "22"
jobs:
@@ -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
@@ -351,7 +360,7 @@ jobs:
sed -i "s|replace git.marketally.com/gitcaddy/gitcaddy-vault => ../gitcaddy-vault|replace git.marketally.com/gitcaddy/gitcaddy-vault => git.marketally.com/gitcaddy/gitcaddy-vault $VAULT_VERSION|" go.mod
fi
cat go.mod | grep -A2 "gitcaddy-vault" || true
go mod tidy -v 2>&1
go mod download
- name: Update vault dependency (Windows)
if: matrix.goos == 'windows'
@@ -366,7 +375,7 @@ jobs:
$content = $content -replace 'replace git.marketally.com/gitcaddy/gitcaddy-vault => ../gitcaddy-vault', "replace git.marketally.com/gitcaddy/gitcaddy-vault => git.marketally.com/gitcaddy/gitcaddy-vault $vaultVersion"
Set-Content go.mod $content -NoNewline
Get-Content go.mod | Select-String "gitcaddy/vault" -Context 0,2
go mod tidy
go mod download
- name: Setup Node.js
uses: actions/setup-node@v4
@@ -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'
@@ -627,7 +637,7 @@ jobs:
echo "Building with vault $VAULT_VERSION"
sed -i "s|replace git.marketally.com/gitcaddy/gitcaddy-vault => ../gitcaddy-vault|replace git.marketally.com/gitcaddy/gitcaddy-vault => git.marketally.com/gitcaddy/gitcaddy-vault $VAULT_VERSION|" go.mod
cat go.mod | grep -A2 "gitcaddy-vault" || true
/usr/local/go/bin/go mod tidy -v 2>&1
/usr/local/go/bin/go mod download
- name: Install pnpm
run: npm install -g pnpm

10
go.mod
View File

@@ -2,7 +2,7 @@ module code.gitcaddy.com/server/v3
go 1.25.0
toolchain go1.25.5
toolchain go1.25.9
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
// But some CAs use negative serial number, just relax the check. related:
@@ -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
@@ -337,4 +340,9 @@ exclude github.com/goccy/go-json v0.4.11
exclude github.com/satori/go.uuid v1.2.0
// Pin SDK to v0.22.0 — v0.24.x requires go 1.26
exclude code.gitea.io/sdk/gitea v0.24.0
exclude code.gitea.io/sdk/gitea v0.24.1
tool code.gitea.io/gitea-vet

6
go.sum
View File

@@ -31,6 +31,10 @@ 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/gitcaddy/gitcaddy-vault v1.0.60 h1:SHmAZG1PxbC9kG9gWdrhrQ7cHVmP0e6iMloBK100kQA=
git.marketally.com/gitcaddy/gitcaddy-vault v1.0.60/go.mod h1:UAmVrJUXHsGe3iJz2gmow2zsIE/KAXaW4jKytrxT8jQ=
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 +494,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

@@ -444,6 +444,7 @@ func prepareMigrationTasks() []*migration {
newMigration(367, "Add pages translation table for multi-language support", v1_26.AddPagesTranslationTable),
newMigration(368, "Add owner_display_name to repository", v1_26.AddOwnerDisplayNameToRepository),
newMigration(369, "Add public_release_downloads to repository", v1_26.AddPublicReleaseDownloadsToRepository),
newMigration(370, "Add exclude_hidden_files to push_mirror", v1_26.AddExcludeHiddenFilesToPushMirror),
}
return preparedMigrations
}

View File

@@ -0,0 +1,16 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"xorm.io/xorm"
)
func AddExcludeHiddenFilesToPushMirror(x *xorm.Engine) error {
type PushMirror struct {
ExcludeHiddenFiles bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync(new(PushMirror))
}

View File

@@ -24,11 +24,12 @@ type PushMirror struct {
RemoteName string
RemoteAddress string `xorm:"VARCHAR(2048)"`
SyncOnCommit bool `xorm:"NOT NULL DEFAULT true"`
Interval time.Duration
CreatedUnix timeutil.TimeStamp `xorm:"created"`
LastUpdateUnix timeutil.TimeStamp `xorm:"INDEX last_update"`
LastError string `xorm:"text"`
SyncOnCommit bool `xorm:"NOT NULL DEFAULT true"`
ExcludeHiddenFiles bool `xorm:"NOT NULL DEFAULT false"`
Interval time.Duration
CreatedUnix timeutil.TimeStamp `xorm:"created"`
LastUpdateUnix timeutil.TimeStamp `xorm:"INDEX last_update"`
LastError string `xorm:"text"`
}
type PushMirrorOptions struct {

View File

@@ -170,6 +170,13 @@ const (
WishlistVoteBudget ErrorCode = "WISHLIST_VOTE_BUDGET_EXCEEDED"
)
// Pages errors (PAGES_)
const (
PagesNotConfigured ErrorCode = "PAGES_NOT_CONFIGURED"
PagesNotEnabled ErrorCode = "PAGES_NOT_ENABLED"
PagesInvalidTemplate ErrorCode = "PAGES_INVALID_TEMPLATE"
)
// AI errors (AI_)
const (
AIDisabled ErrorCode = "AI_DISABLED"
@@ -310,6 +317,11 @@ var errorCatalog = map[ErrorCode]errorInfo{
WishlistDisabled: {"Wishlist is disabled for this repository", http.StatusForbidden},
WishlistVoteBudget: {"Vote budget exceeded for this repository", http.StatusConflict},
// Pages errors
PagesNotConfigured: {"Landing page is not configured for this repository", http.StatusNotFound},
PagesNotEnabled: {"Landing page is not enabled for this repository", http.StatusForbidden},
PagesInvalidTemplate: {"Invalid landing page template name", http.StatusBadRequest},
// AI errors
AIDisabled: {"AI features are disabled", http.StatusForbidden},
AIUnitNotEnabled: {"AI unit is not enabled for this repository", http.StatusForbidden},

156
modules/git/tree_filter.go Normal file
View File

@@ -0,0 +1,156 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"bytes"
"fmt"
"strings"
"code.gitcaddy.com/server/v3/modules/git/gitcmd"
)
// FilterTreeOptions controls which paths are excluded when building a filtered tree.
type FilterTreeOptions struct {
// ExcludeDotFiles excludes all entries whose name starts with "."
ExcludeDotFiles bool
// ExcludePaths is a set of specific paths to exclude (exact top-level names)
ExcludePaths map[string]bool
}
// ShouldExclude returns true if the given entry name should be filtered out.
func (opts *FilterTreeOptions) ShouldExclude(name string) bool {
if opts.ExcludeDotFiles && strings.HasPrefix(name, ".") {
return true
}
if opts.ExcludePaths != nil && opts.ExcludePaths[name] {
return true
}
return false
}
// HasFilters returns true if any filtering is configured.
func (opts *FilterTreeOptions) HasFilters() bool {
return opts.ExcludeDotFiles || len(opts.ExcludePaths) > 0
}
// BuildFilteredCommit creates a new commit that has the same content as the
// given commit but with excluded paths removed from the top-level tree.
// It uses git plumbing (ls-tree, mktree, commit-tree) and creates no refs —
// only loose objects that git gc will collect naturally.
// Returns the SHA of the new commit, or the original SHA if nothing was filtered.
func (repo *Repository) BuildFilteredCommit(commitSHA string, opts *FilterTreeOptions) (string, error) {
if !opts.HasFilters() {
return commitSHA, nil
}
// 1. Get the top-level tree entries for this commit
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
err := gitcmd.NewCommand("ls-tree").AddArguments("--").AddDynamicArguments(commitSHA).
WithDir(repo.Path).
WithStdout(stdout).
WithStderr(stderr).
Run(repo.Ctx)
if err != nil {
return "", fmt.Errorf("ls-tree %s: %s: %w", commitSHA, stderr.String(), err)
}
// 2. Filter entries
var filteredLines []string
excluded := false
for line := range strings.SplitSeq(strings.TrimRight(stdout.String(), "\n"), "\n") {
if line == "" {
continue
}
// ls-tree output format: "<mode> <type> <sha>\t<name>"
tabIdx := strings.IndexByte(line, '\t')
if tabIdx < 0 {
continue
}
name := line[tabIdx+1:]
if opts.ShouldExclude(name) {
excluded = true
continue
}
filteredLines = append(filteredLines, line)
}
// Nothing was filtered — return original commit
if !excluded {
return commitSHA, nil
}
// 3. Create a new tree from filtered entries via mktree
treeInput := strings.Join(filteredLines, "\n") + "\n"
stdout.Reset()
stderr.Reset()
err = gitcmd.NewCommand("mktree").
WithDir(repo.Path).
WithStdin(strings.NewReader(treeInput)).
WithStdout(stdout).
WithStderr(stderr).
Run(repo.Ctx)
if err != nil {
return "", fmt.Errorf("mktree: %s: %w", stderr.String(), err)
}
newTreeSHA := strings.TrimSpace(stdout.String())
// 4. Get the original commit's parent(s) and message
stdout.Reset()
stderr.Reset()
err = gitcmd.NewCommand("log", "-1", "--format=%P%n%B").AddArguments("--").AddDynamicArguments(commitSHA).
WithDir(repo.Path).
WithStdout(stdout).
WithStderr(stderr).
Run(repo.Ctx)
if err != nil {
return "", fmt.Errorf("log %s: %s: %w", commitSHA, stderr.String(), err)
}
logOutput := stdout.String()
lines := strings.SplitN(logOutput, "\n", 2)
parentLine := ""
message := "filtered commit"
if len(lines) >= 1 {
parentLine = strings.TrimSpace(lines[0])
}
if len(lines) >= 2 {
message = strings.TrimRight(lines[1], "\n")
}
// 5. Create a new commit referencing the filtered tree
cmd := gitcmd.NewCommand("commit-tree").AddDynamicArguments(newTreeSHA)
for parent := range strings.FieldsSeq(parentLine) {
cmd = cmd.AddArguments("-p").AddDynamicArguments(parent)
}
messageReader := strings.NewReader(message + "\n")
stdout.Reset()
stderr.Reset()
err = cmd.
WithDir(repo.Path).
WithStdin(messageReader).
WithStdout(stdout).
WithStderr(stderr).
Run(repo.Ctx)
if err != nil {
return "", fmt.Errorf("commit-tree: %s: %w", stderr.String(), err)
}
return strings.TrimSpace(stdout.String()), nil
}
// UpdateRef updates a ref to point at the given SHA.
func (repo *Repository) UpdateRef(ref, sha string) error {
stderr := new(bytes.Buffer)
err := gitcmd.NewCommand("update-ref").AddDynamicArguments(ref, sha).
WithDir(repo.Path).
WithStderr(stderr).
Run(repo.Ctx)
if err != nil {
return fmt.Errorf("update-ref %s %s: %s: %w", ref, sha, stderr.String(), err)
}
return nil
}

View File

@@ -13,80 +13,87 @@ import (
// 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
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"`
Domain string `yaml:"domain,omitempty" json:"domain,omitempty"`
// Brand configuration
Brand BrandConfig `yaml:"brand,omitempty"`
Brand BrandConfig `yaml:"brand,omitempty" json:"brand,omitzero"`
// Hero section
Hero HeroConfig `yaml:"hero,omitempty"`
Hero HeroConfig `yaml:"hero,omitempty" json:"hero,omitzero"`
// Stats/metrics
Stats []StatConfig `yaml:"stats,omitempty"`
Stats []StatConfig `yaml:"stats,omitempty" json:"stats,omitempty"`
// Value propositions
ValueProps []ValuePropConfig `yaml:"value_props,omitempty"`
ValueProps []ValuePropConfig `yaml:"value_props,omitempty" json:"value_props,omitempty"`
ValuePropsHeadline string `yaml:"value_props_headline,omitempty" json:"value_props_headline,omitempty"`
ValuePropsSubheadline string `yaml:"value_props_subheadline,omitempty" json:"value_props_subheadline,omitempty"`
// Features
Features []FeatureConfig `yaml:"features,omitempty"`
Features []FeatureConfig `yaml:"features,omitempty" json:"features,omitempty"`
FeaturesHeadline string `yaml:"features_headline,omitempty" json:"features_headline,omitempty"`
FeaturesSubheadline string `yaml:"features_subheadline,omitempty" json:"features_subheadline,omitempty"`
// Social proof
SocialProof SocialProofConfig `yaml:"social_proof,omitempty"`
SocialProof SocialProofConfig `yaml:"social_proof,omitempty" json:"social_proof,omitzero"`
// Pricing (for saas-conversion template)
Pricing PricingConfig `yaml:"pricing,omitempty"`
Pricing PricingConfig `yaml:"pricing,omitempty" json:"pricing,omitzero"`
// CTA section
CTASection CTASectionConfig `yaml:"cta_section,omitempty"`
CTASection CTASectionConfig `yaml:"cta_section,omitempty" json:"cta_section,omitzero"`
// Blog section
Blog BlogSectionConfig `yaml:"blog,omitempty"`
Blog BlogSectionConfig `yaml:"blog,omitempty" json:"blog,omitzero"`
// Gallery section
Gallery GallerySectionConfig `yaml:"gallery,omitempty"`
Gallery GallerySectionConfig `yaml:"gallery,omitempty" json:"gallery,omitzero"`
// Comparison section
Comparison ComparisonSectionConfig `yaml:"comparison,omitempty"`
Comparison ComparisonSectionConfig `yaml:"comparison,omitempty" json:"comparison,omitzero"`
// Cross-promote section
CrossPromote CrossPromoteSectionConfig `yaml:"cross_promote,omitempty" json:"cross_promote,omitzero"`
// Navigation visibility
Navigation NavigationConfig `yaml:"navigation,omitempty"`
Navigation NavigationConfig `yaml:"navigation,omitempty" json:"navigation,omitzero"`
// Footer
Footer FooterConfig `yaml:"footer,omitempty"`
Footer FooterConfig `yaml:"footer,omitempty" json:"footer,omitzero"`
// Theme customization
Theme ThemeConfig `yaml:"theme,omitempty"`
Theme ThemeConfig `yaml:"theme,omitempty" json:"theme,omitzero"`
// SEO & Social
SEO SEOConfig `yaml:"seo,omitempty"`
SEO SEOConfig `yaml:"seo,omitempty" json:"seo,omitzero"`
// Analytics
Analytics AnalyticsConfig `yaml:"analytics,omitempty"`
Analytics AnalyticsConfig `yaml:"analytics,omitempty" json:"analytics,omitzero"`
// Advanced settings
Advanced AdvancedConfig `yaml:"advanced,omitempty"`
Advanced AdvancedConfig `yaml:"advanced,omitempty" json:"advanced,omitzero"`
// A/B testing experiments
Experiments ExperimentConfig `yaml:"experiments,omitempty"`
Experiments ExperimentConfig `yaml:"experiments,omitempty" json:"experiments,omitzero"`
// Multi-language support
I18n I18nConfig `yaml:"i18n,omitempty"`
I18n I18nConfig `yaml:"i18n,omitempty" json:"i18n,omitzero"`
}
// 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"`
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.
@@ -107,14 +114,14 @@ func (b *BrandConfig) ResolvedFaviconURL() string {
// 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"`
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.
@@ -127,97 +134,97 @@ func (h *HeroConfig) ResolvedImageURL() string {
// 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
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"`
Label string `yaml:"label,omitempty"`
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"`
Description string `yaml:"description,omitempty"`
Icon string `yaml:"icon,omitempty"`
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"`
Description string `yaml:"description,omitempty"`
Icon string `yaml:"icon,omitempty"`
ImageURL string `yaml:"image_url,omitempty"`
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"`
Testimonial TestimonialConfig `yaml:"testimonial,omitempty"`
Testimonials []TestimonialConfig `yaml:"testimonials,omitempty"`
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"`
Author string `yaml:"author,omitempty"`
Role string `yaml:"role,omitempty"`
Avatar string `yaml:"avatar,omitempty"`
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"`
Subheadline string `yaml:"subheadline,omitempty"`
Plans []PricingPlanConfig `yaml:"plans,omitempty"`
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"`
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"`
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"`
Subheadline string `yaml:"subheadline,omitempty"`
Button CTAButton `yaml:"button,omitempty"`
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"`
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
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"`
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
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"`
Headline string `yaml:"headline,omitempty"`
Subheadline string `yaml:"subheadline,omitempty"`
Columns []ComparisonColumnConfig `yaml:"columns,omitempty"`
Groups []ComparisonGroupConfig `yaml:"groups,omitempty"`
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
@@ -235,88 +242,107 @@ func (c *ComparisonSectionConfig) HasData() bool {
// ComparisonColumnConfig represents a column header in the comparison table
type ComparisonColumnConfig struct {
Name string `yaml:"name,omitempty"`
Highlight bool `yaml:"highlight,omitempty"`
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"`
Features []ComparisonFeatureConfig `yaml:"features,omitempty"`
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"`
Values []string `yaml:"values,omitempty"` // "true"/"false" for check/x, anything else displayed as text
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
}
// CrossPromoteSectionConfig controls the cross-promote section on the landing page
type CrossPromoteSectionConfig 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"`
}
// 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"`
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"`
LabelCrossPromote string `yaml:"label_cross_promote,omitempty" json:"label_cross_promote,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"`
Social []SocialLink `yaml:"social,omitempty"`
Copyright string `yaml:"copyright,omitempty"`
ShowPoweredBy bool `yaml:"show_powered_by,omitempty"`
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"`
URL string `yaml:"url,omitempty"`
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"` // bluesky, discord, facebook, github, instagram, linkedin, mastodon, reddit, rss, substack, threads, tiktok, twitch, twitter, youtube
URL string `yaml:"url,omitempty"`
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"`
AccentColor string `yaml:"accent_color,omitempty"`
Mode string `yaml:"mode,omitempty"` // light, dark, auto
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"`
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"`
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"`
Umami UmamiConfig `yaml:"umami,omitempty"`
GoogleAnalytics string `yaml:"google_analytics,omitempty"`
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"`
URL string `yaml:"url,omitempty"`
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"`
AutoOptimize bool `yaml:"auto_optimize,omitempty"`
MinImpressions int `yaml:"min_impressions,omitempty"`
ApprovalRequired bool `yaml:"approval_required,omitempty"`
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
@@ -325,36 +351,37 @@ type I18nConfig struct {
Languages []string `yaml:"languages,omitempty" json:"languages,omitempty"`
}
// LanguageDisplayNames returns a map of language codes to display names
// LanguageDisplayNames returns a map of language codes to native display names
func LanguageDisplayNames() map[string]string {
return map[string]string{
"en": "English",
"es": "Espanol",
"es": "Español",
"de": "Deutsch",
"fr": "Francais",
"ja": "Japanese",
"zh": "Chinese",
"pt": "Portugues",
"ru": "Russian",
"ko": "Korean",
"fr": "Français",
"ja": "日本語",
"zh": "中文",
"pt": "Português",
"ru": "Русский",
"ko": "한국어",
"it": "Italiano",
"hi": "Hindi",
"ar": "Arabic",
"hi": "हिन्दी",
"ar": "العربية",
"nl": "Nederlands",
"pl": "Polski",
"tr": "Turkish",
"tr": "Türkçe",
}
}
// 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"`
CustomCSS string `yaml:"custom_css,omitempty" json:"custom_css,omitempty"`
CustomHead string `yaml:"custom_head,omitempty" json:"custom_head,omitempty"`
Redirects map[string]string `yaml:"redirects,omitempty" json:"redirects,omitempty"`
StaticRoutes []string `yaml:"static_routes,omitempty" json:"static_routes,omitempty"`
PublicReleases bool `yaml:"public_releases,omitempty" json:"public_releases,omitempty"`
HideMobileReleases bool `yaml:"hide_mobile_releases,omitempty" json:"hide_mobile_releases,omitempty"`
GooglePlayID string `yaml:"google_play_id,omitempty" json:"google_play_id,omitempty"`
AppStoreID string `yaml:"app_store_id,omitempty" json:"app_store_id,omitempty"`
}
// ParseLandingConfig parses a landing.yaml file content
@@ -458,3 +485,71 @@ func TemplateDisplayNames() map[string]string {
"architecture-deep-dive": "Architecture Deep Dive",
}
}
// TemplateDefaultLabels returns the template-specific default section labels.
// These are the creative names each template uses for its sections.
func TemplateDefaultLabels(template string) NavigationConfig {
switch template {
case "architecture-deep-dive":
return NavigationConfig{
LabelValueProps: "Systems Analysis",
LabelFeatures: "Technical Specifications",
LabelPricing: "Resource Allocation",
LabelBlog: "Dispatches",
LabelGallery: "Visual Index",
LabelCompare: "Compare",
LabelCrossPromote: "Related Offerings",
}
case "bold-marketing":
return NavigationConfig{
LabelValueProps: "Why choose this",
LabelFeatures: "Capabilities",
LabelPricing: "Investment",
LabelBlog: "Blog",
LabelGallery: "Gallery",
LabelCompare: "Compare",
LabelCrossPromote: "Related Offerings",
}
case "minimalist-docs":
return NavigationConfig{
LabelValueProps: "Why choose this",
LabelFeatures: "Capabilities",
LabelPricing: "Investment",
LabelBlog: "Blog",
LabelGallery: "Gallery",
LabelCompare: "Compare",
LabelCrossPromote: "Related Offerings",
}
case "open-source-hero":
return NavigationConfig{
LabelValueProps: "Why choose us",
LabelFeatures: "Capabilities",
LabelPricing: "Pricing",
LabelBlog: "Blog",
LabelGallery: "Gallery",
LabelCompare: "Compare",
LabelCrossPromote: "Related Offerings",
}
case "saas-conversion":
return NavigationConfig{
LabelValueProps: "Why",
LabelFeatures: "Features",
LabelPricing: "Pricing",
LabelBlog: "Blog",
LabelGallery: "Gallery",
LabelCompare: "Compare",
LabelCrossPromote: "Related Offerings",
}
default:
// developer-tool, documentation-first, visual-showcase, cli-terminal
return NavigationConfig{
LabelValueProps: "Why choose us",
LabelFeatures: "Capabilities",
LabelPricing: "Pricing",
LabelBlog: "Blog",
LabelGallery: "Gallery",
LabelCompare: "Compare",
LabelCrossPromote: "Related Offerings",
}
}
}

View File

@@ -191,3 +191,25 @@ type OrgProfileContent struct {
Readme string `json:"readme,omitempty"`
HasCSS bool `json:"has_css"`
}
// OrgRecentActivity represents a recently updated repo in the org overview
type OrgRecentActivity struct {
RepoName string `json:"repo_name"`
RepoFullName string `json:"repo_full_name"`
DefaultBranch string `json:"default_branch"`
CommitMessage string `json:"commit_message,omitempty"`
CommitTime int64 `json:"commit_time,omitempty"`
IsPrivate bool `json:"is_private"`
}
// OrgOverviewV2 represents the enhanced organization overview for v2 API
type OrgOverviewV2 struct {
Organization *Organization `json:"organization"`
PinnedRepos []*OrgPinnedRepo `json:"pinned_repos"`
PinnedGroups []*OrgPinnedGroup `json:"pinned_groups"`
PublicMembers []*OrgPublicMember `json:"public_members"`
TotalMembers int64 `json:"total_members"`
Stats *OrgOverviewStats `json:"stats"`
Profile *OrgProfileContent `json:"profile,omitempty"`
RecentActivity []OrgRecentActivity `json:"recent_activity,omitempty"`
}

View File

@@ -47,3 +47,231 @@ type PagesInfo struct {
Config *PagesConfig `json:"config"`
Domains []*PagesDomain `json:"domains,omitempty"`
}
// ---------------------------------------------------------------------------
// v2 Landing Page Configuration API structs
// ---------------------------------------------------------------------------
// UpdatePagesConfigOption represents the full landing page config update.
// For PATCH, only non-nil fields are applied. For PUT, all fields replace existing config.
type UpdatePagesConfigOption struct {
Enabled *bool `json:"enabled"`
PublicLanding *bool `json:"public_landing"`
Template *string `json:"template"`
Brand *UpdatePagesBrandOption `json:"brand"`
Hero *UpdatePagesHeroOption `json:"hero"`
Stats *[]PagesStatOption `json:"stats"`
ValueProps *[]PagesValuePropOption `json:"value_props"`
Features *[]PagesFeatureOption `json:"features"`
SocialProof *UpdatePagesSocialOption `json:"social_proof"`
Pricing *UpdatePagesPricingOption `json:"pricing"`
CTASection *UpdatePagesCTAOption `json:"cta_section"`
Blog *UpdatePagesBlogOption `json:"blog"`
Gallery *UpdatePagesGalleryOption `json:"gallery"`
Comparison *UpdatePagesComparisonOption `json:"comparison"`
Navigation *UpdatePagesNavOption `json:"navigation"`
Footer *UpdatePagesFooterOption `json:"footer"`
Theme *UpdatePagesThemeOption `json:"theme"`
SEO *UpdatePagesSEOOption `json:"seo"`
Advanced *UpdatePagesAdvancedOption `json:"advanced"`
}
// UpdatePagesBrandOption represents brand section update
type UpdatePagesBrandOption struct {
Name *string `json:"name"`
LogoURL *string `json:"logo_url"`
Tagline *string `json:"tagline"`
FaviconURL *string `json:"favicon_url"`
}
// UpdatePagesHeroOption represents hero section update
type UpdatePagesHeroOption struct {
Headline *string `json:"headline"`
Subheadline *string `json:"subheadline"`
ImageURL *string `json:"image_url"`
VideoURL *string `json:"video_url"`
CodeExample *string `json:"code_example"`
PrimaryCTA *PagesCTAButtonOption `json:"primary_cta"`
SecondaryCTA *PagesCTAButtonOption `json:"secondary_cta"`
}
// PagesCTAButtonOption represents a CTA button
type PagesCTAButtonOption struct {
Label *string `json:"label"`
URL *string `json:"url"`
Variant *string `json:"variant"`
}
// PagesStatOption represents a stat item
type PagesStatOption struct {
Value string `json:"value"`
Label string `json:"label"`
}
// PagesValuePropOption represents a value proposition item
type PagesValuePropOption struct {
Title string `json:"title"`
Description string `json:"description"`
Icon string `json:"icon"`
}
// PagesFeatureOption represents a feature item
type PagesFeatureOption struct {
Title string `json:"title"`
Description string `json:"description"`
Icon string `json:"icon"`
ImageURL string `json:"image_url"`
}
// UpdatePagesSocialOption represents social proof section update
type UpdatePagesSocialOption struct {
Logos *[]string `json:"logos"`
Testimonials *[]PagesTestimonialOption `json:"testimonials"`
}
// PagesTestimonialOption represents a testimonial item
type PagesTestimonialOption struct {
Quote string `json:"quote"`
Author string `json:"author"`
Role string `json:"role"`
Avatar string `json:"avatar"`
}
// UpdatePagesPricingOption represents pricing section update
type UpdatePagesPricingOption struct {
Headline *string `json:"headline"`
Subheadline *string `json:"subheadline"`
Plans *[]PagesPricingPlanOption `json:"plans"`
}
// PagesPricingPlanOption represents a pricing plan item
type PagesPricingPlanOption struct {
Name string `json:"name"`
Price string `json:"price"`
Period string `json:"period"`
Features []string `json:"features"`
CTA string `json:"cta"`
Featured bool `json:"featured"`
}
// UpdatePagesCTAOption represents CTA section update
type UpdatePagesCTAOption struct {
Headline *string `json:"headline"`
Subheadline *string `json:"subheadline"`
Button *PagesCTAButtonOption `json:"button"`
}
// UpdatePagesBlogOption represents blog section update
type UpdatePagesBlogOption struct {
Enabled *bool `json:"enabled"`
Headline *string `json:"headline"`
Subheadline *string `json:"subheadline"`
MaxPosts *int `json:"max_posts"`
}
// UpdatePagesGalleryOption represents gallery section update
type UpdatePagesGalleryOption struct {
Enabled *bool `json:"enabled"`
Headline *string `json:"headline"`
Subheadline *string `json:"subheadline"`
MaxImages *int `json:"max_images"`
Columns *int `json:"columns"`
}
// UpdatePagesComparisonOption represents comparison section update
type UpdatePagesComparisonOption struct {
Enabled *bool `json:"enabled"`
Headline *string `json:"headline"`
Subheadline *string `json:"subheadline"`
Columns *[]PagesComparisonColumnOption `json:"columns"`
Groups *[]PagesComparisonGroupOption `json:"groups"`
}
// PagesComparisonColumnOption represents a comparison column
type PagesComparisonColumnOption struct {
Name string `json:"name"`
Highlight bool `json:"highlight"`
}
// PagesComparisonGroupOption represents a comparison group
type PagesComparisonGroupOption struct {
Name string `json:"name"`
Features []PagesComparisonFeatureOption `json:"features"`
}
// PagesComparisonFeatureOption represents a comparison feature row
type PagesComparisonFeatureOption struct {
Name string `json:"name"`
Values []string `json:"values"`
}
// UpdatePagesNavOption represents navigation section update
type UpdatePagesNavOption struct {
ShowDocs *bool `json:"show_docs"`
ShowAPI *bool `json:"show_api"`
ShowRepository *bool `json:"show_repository"`
ShowReleases *bool `json:"show_releases"`
ShowIssues *bool `json:"show_issues"`
}
// UpdatePagesFooterOption represents footer section update
type UpdatePagesFooterOption struct {
Copyright *string `json:"copyright"`
ShowPoweredBy *bool `json:"show_powered_by"`
Links *[]PagesFooterLinkOption `json:"links"`
Social *[]PagesSocialLinkOption `json:"social"`
CTASection *UpdatePagesCTAOption `json:"cta_section"`
}
// PagesFooterLinkOption represents a footer link
type PagesFooterLinkOption struct {
Label string `json:"label"`
URL string `json:"url"`
}
// PagesSocialLinkOption represents a social link
type PagesSocialLinkOption struct {
Platform string `json:"platform"`
URL string `json:"url"`
}
// UpdatePagesThemeOption represents theme section update
type UpdatePagesThemeOption struct {
PrimaryColor *string `json:"primary_color"`
AccentColor *string `json:"accent_color"`
Mode *string `json:"mode"`
}
// UpdatePagesSEOOption represents SEO section update
type UpdatePagesSEOOption struct {
Title *string `json:"title"`
Description *string `json:"description"`
Keywords *[]string `json:"keywords"`
OGImage *string `json:"og_image"`
UseMediaKitOG *bool `json:"use_media_kit_og"`
TwitterCard *string `json:"twitter_card"`
TwitterSite *string `json:"twitter_site"`
}
// UpdatePagesAdvancedOption represents advanced settings update
type UpdatePagesAdvancedOption struct {
CustomCSS *string `json:"custom_css"`
CustomHead *string `json:"custom_head"`
StaticRoutes *[]string `json:"static_routes"`
PublicReleases *bool `json:"public_releases"`
HideMobileReleases *bool `json:"hide_mobile_releases"`
GooglePlayID *string `json:"google_play_id"`
AppStoreID *string `json:"app_store_id"`
}
// UpdatePagesContentOption bundles content-page sections for PUT /config/content
type UpdatePagesContentOption struct {
Blog *UpdatePagesBlogOption `json:"blog"`
Gallery *UpdatePagesGalleryOption `json:"gallery"`
ComparisonEnabled *bool `json:"comparison_enabled"`
Stats *[]PagesStatOption `json:"stats"`
ValueProps *[]PagesValuePropOption `json:"value_props"`
Features *[]PagesFeatureOption `json:"features"`
Navigation *UpdatePagesNavOption `json:"navigation"`
Advanced *UpdatePagesAdvancedOption `json:"advanced"`
}

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -2303,6 +2303,8 @@
"repo.settings.mirror_settings.push_mirror.none": "No push mirrors configured",
"repo.settings.mirror_settings.push_mirror.remote_url": "Git Remote Repository URL",
"repo.settings.mirror_settings.push_mirror.add": "Add Push Mirror",
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files": "Exclude hidden files and folders",
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files_desc": "Do not push files/folders starting with \".\" or marked as hidden to the remote mirror",
"repo.settings.mirror_settings.push_mirror.edit_sync_time": "Edit mirror sync interval",
"repo.settings.sync_mirror": "Synchronize Now",
"repo.settings.pull_mirror_sync_in_progress": "Pulling changes from the remote %s at the moment.",
@@ -4503,6 +4505,12 @@
"repo.settings.pages.app_store_id_desc": "App ID from the App Store URL (e.g. id123456789)",
"repo.settings.pages.stats": "Stats",
"repo.settings.pages.value_props": "Value Propositions",
"repo.settings.pages.value_props_headline": "Section Headline",
"repo.settings.pages.value_props_headline_help": "Large heading for the value propositions section (e.g., \"Built for makers\"). The small label above it is set in Section Labels.",
"repo.settings.pages.value_props_subheadline": "Section Subheadline",
"repo.settings.pages.features_headline": "Section Headline",
"repo.settings.pages.features_headline_help": "Large heading for the features section (e.g., \"Everything you need\"). The small label above it is set in Section Labels.",
"repo.settings.pages.features_subheadline": "Section Subheadline",
"repo.settings.pages.features": "Features",
"repo.settings.pages.company_logos": "Company Logos",
"repo.settings.pages.testimonials": "Testimonials",
@@ -4545,6 +4553,12 @@
"repo.settings.pages.nav_show_repository": "Show Repository link (View Source button)",
"repo.settings.pages.nav_show_releases": "Show Releases link",
"repo.settings.pages.nav_show_issues": "Show Issues link",
"repo.settings.pages.section_labels": "Section Labels",
"repo.settings.pages.section_labels_desc": "Customize the headings shown on your landing page for each section",
"repo.settings.pages.label_value_props": "Value Props Heading",
"repo.settings.pages.label_value_props_help": "Heading for the value propositions section (e.g., \"Why choose us\")",
"repo.settings.pages.label_features": "Features Heading",
"repo.settings.pages.label_features_help": "Heading for the features section (e.g., \"Capabilities\")",
"repo.settings.pages.blog_section": "Blog Section",
"repo.settings.pages.blog_enabled_desc": "Show recent blog posts on the landing page",
"repo.settings.pages.blog_headline": "Blog Headline",
@@ -4558,6 +4572,11 @@
"repo.settings.pages.gallery_columns": "Grid Columns",
"repo.settings.pages.gallery_help_link": "Upload and manage gallery images in Settings > Gallery.",
"repo.settings.pages.comparison": "Comparison",
"repo.settings.pages.cross_promote_section": "Cross-Promote Section",
"repo.settings.pages.cross_promote_enabled_desc": "Show cross-promoted repositories on the landing page (only repos with landing pages enabled are shown)",
"repo.settings.pages.cross_promote_headline": "Section Headline",
"repo.settings.pages.cross_promote_subheadline": "Section Subheadline",
"repo.settings.pages.cross_promote_help": "Configure which repositories to cross-promote in Settings > Cross-Promote. Only repos with landing pages enabled will appear.",
"repo.settings.pages.comparison_section": "Comparison Section",
"repo.settings.pages.comparison_enabled_desc": "Show a feature comparison table on the landing page",
"repo.settings.pages.comparison_headline": "Headline",
@@ -4572,6 +4591,7 @@
"repo.settings.pages.ai_generate": "AI Content Generator",
"repo.settings.pages.ai_generate_desc": "Automatically generate landing page content (headline, features, stats, CTAs) from your repository's README and metadata using AI.",
"repo.settings.pages.ai_generate_button": "Generate Content with AI",
"repo.settings.pages.ai_generating": "Generating content with AI\u2026 This may take a moment.",
"repo.settings.pages.ai_generate_success": "Landing page content has been generated successfully. Review and customize it in the other tabs.",
"repo.settings.pages.ai_generate_failed": "Failed to generate content with AI. Please try again later or configure the content manually.",
"repo.settings.pages.languages": "Languages",
@@ -4584,6 +4604,10 @@
"repo.settings.pages.translations": "Translations",
"repo.settings.pages.ai_translate": "AI Translate",
"repo.settings.pages.ai_translate_success": "Translation has been generated successfully by AI. Review and edit as needed.",
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
"repo.settings.pages.ai_translating": "Translating with AI\u2026 This may take a moment.",
"repo.settings.pages.delete_translation": "Delete",
"repo.settings.pages.save_translation": "Save Translation",
"repo.settings.pages.translation_saved": "Translation saved successfully.",
@@ -4596,6 +4620,57 @@
"repo.settings.pages.trans_cta_headline": "CTA Section Headline",
"repo.settings.pages.trans_cta_subheadline": "CTA Section Subheadline",
"repo.settings.pages.trans_cta_button": "CTA Button Label",
"repo.settings.pages.trans_section_brand": "Brand",
"repo.settings.pages.trans_section_hero": "Hero",
"repo.settings.pages.trans_section_stats": "Stats",
"repo.settings.pages.trans_section_value_props": "Value Propositions",
"repo.settings.pages.trans_section_features": "Features",
"repo.settings.pages.trans_section_testimonials": "Testimonials",
"repo.settings.pages.trans_section_pricing": "Pricing",
"repo.settings.pages.trans_section_cta": "Call to Action",
"repo.settings.pages.trans_section_blog": "Blog",
"repo.settings.pages.trans_section_gallery": "Gallery",
"repo.settings.pages.trans_section_comparison": "Comparison",
"repo.settings.pages.trans_section_footer": "Footer",
"repo.settings.pages.trans_section_seo": "SEO",
"repo.settings.pages.trans_section_navigation": "Navigation Labels",
"repo.settings.pages.trans_nav_label": "Label",
"repo.settings.pages.trans_brand_name": "Brand Name",
"repo.settings.pages.trans_brand_tagline": "Tagline",
"repo.settings.pages.trans_stat_value": "Value",
"repo.settings.pages.trans_stat_label": "Label",
"repo.settings.pages.trans_title": "Title",
"repo.settings.pages.trans_description": "Description",
"repo.settings.pages.trans_quote": "Quote",
"repo.settings.pages.trans_role": "Role",
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
"repo.settings.pages.trans_plan_name": "Plan Name",
"repo.settings.pages.trans_plan_period": "Period",
"repo.settings.pages.trans_plan_cta": "Plan Button",
"repo.settings.pages.trans_blog_headline": "Blog Headline",
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
"repo.settings.pages.trans_blog_cta": "Blog Button",
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
"repo.settings.pages.trans_footer_copyright": "Copyright",
"repo.settings.pages.trans_footer_link": "Link Label",
"repo.settings.pages.trans_seo_title": "SEO Title",
"repo.settings.pages.trans_seo_description": "SEO Description",
"repo.settings.pages.advanced": "Advanced",
"repo.settings.pages.static_routes": "Static Routes",
"repo.settings.pages.static_routes_desc": "Serve files directly from your repository at specific URL paths instead of rendering the landing page. Use exact paths (/badge.svg) or glob patterns (/schema/*).",
"repo.settings.pages.add_route": "Add Route",
"repo.settings.pages.redirects": "Redirects",
"repo.settings.pages.redirects_desc": "Redirect specific paths to other URLs. The source path must start with /.",
"repo.settings.pages.add_redirect": "Add Redirect",
"repo.settings.pages.custom_code": "Custom Code",
"repo.settings.pages.custom_css": "Custom CSS",
"repo.settings.pages.custom_head": "Custom Head HTML",
"repo.settings.pages.app_stores": "App Stores",
"repo.settings.pages.hide_mobile_releases": "Hide Mobile Releases",
"repo.vault": "Vault",
"repo.vault.secrets": "Secrets",
"repo.vault.new_secret": "New Secret",

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -15,8 +15,8 @@
"template": "Template",
"language": "Bahasa",
"notifications": "Notifikasi",
"create_new": "Buat…",
"user_profile_and_more": "Profil dan Pengaturan…",
"create_new": "Buat\u00e2\u20ac\u00a6",
"user_profile_and_more": "Profil dan Pengaturan\u00e2\u20ac\u00a6",
"signed_in_as": "Masuk sebagai",
"toc": "Daftar Isi",
"username": "Nama Pengguna",
@@ -28,7 +28,7 @@
"passcode": "Kode Akses",
"webauthn_insert_key": "Masukkan kunci keamanan anda",
"webauthn_sign_in": "Tekan tombol pada kunci keamanan Anda. Jika kunci keamanan Anda tidak memiliki tombol, masukkan kembali.",
"webauthn_press_button": "Silakan tekan tombol pada kunci keamanan Anda…",
"webauthn_press_button": "Silakan tekan tombol pada kunci keamanan Anda\u00e2\u20ac\u00a6",
"webauthn_use_twofa": "Gunakan kode dua faktor dari telepon Anda",
"webauthn_error": "Tidak dapat membaca kunci keamanan Anda.",
"webauthn_unsupported_browser": "Browser Anda saat ini tidak mendukung WebAuthn.",
@@ -90,7 +90,7 @@
"copy_type_unsupported": "Tipe berkas ini tidak dapat disalin",
"write": "Tulis",
"preview": "Pratinjau",
"loading": "Memuat…",
"loading": "Memuat\u00e2\u20ac\u00a6",
"error_title": "Gangguan",
"error404": "Halaman yang akan kamu akses <strong>tidak dapat ditemukan</strong> atau <strong>kamu tidak memiliki akses </strong> untuk melihatnya.",
"go_back": "Kembali",
@@ -175,7 +175,7 @@
"home.password_holder": "Kata Sandi",
"home.switch_dashboard_context": "Alihkan Dasbor Konteks",
"home.my_repos": "Repositori",
"home.show_more_repos": "Tampilkan repositori lainnya…",
"home.show_more_repos": "Tampilkan repositori lainnya\u00e2\u20ac\u00a6",
"home.collaborative_repos": "Repositori Kolaboratif",
"home.my_orgs": "Organisasi Saya",
"home.my_mirrors": "Duplikat Saya",
@@ -295,7 +295,7 @@
"form.invalid_gpg_key": "Tidak dapat memverifikasi kunci GPG Anda: %s",
"form.auth_failed": "Otentikasi gagal: %v",
"form.target_branch_not_exist": "Target cabang tidak ada.",
"user.change_avatar": "Ganti avatar anda…",
"user.change_avatar": "Ganti avatar anda\u00e2\u20ac\u00a6",
"user.repositories": "Repositori",
"user.activity": "Aktivitas Publik",
"user.followers": "Pengikut",
@@ -564,18 +564,18 @@
"repo.editor.fork_before_edit": "Anda harus mencabangkan repositori ini untuk membuat atau mengusulkan perubahan pada berkas ini.",
"repo.editor.delete_this_file": "Hapus Berkas",
"repo.editor.must_have_write_access": "Anda harus punya akses tulis untuk membuat atau mengusulkan perubahan pada berkas ini.",
"repo.editor.name_your_file": "Nama berkas…",
"repo.editor.name_your_file": "Nama berkas\u00e2\u20ac\u00a6",
"repo.editor.filename_help": "Tambahkan direktori dengan mengetikkan nama direktori diikuti dengan garis miring ('/'). Hapus direktori dengan mengetikkan spasi balik pada awal bidang input.",
"repo.editor.or": "atau",
"repo.editor.cancel_lower": "Batalkan",
"repo.editor.commit_changes": "Perubahan komitmen",
"repo.editor.commit_message": "Commit message",
"repo.editor.commit_message_desc": "Tambahkan deskripsi opsional yang panjang…",
"repo.editor.commit_message_desc": "Tambahkan deskripsi opsional yang panjang\u00e2\u20ac\u00a6",
"repo.editor.commit_directly_to_this_branch": "Komitmen langsung ke <strong class=\"branch-name\">%s</strong> cabang.",
"repo.editor.create_new_branch": "Membuat <strong>new branch</strong> untuk tarik komit ini mulai permintaan.",
"repo.editor.create_new_branch_np": "Buat <strong>cabang baru</strong> untuk komit ini.",
"repo.editor.propose_file_change": "Usul perubahan berkas",
"repo.editor.new_branch_name_desc": "Nama branch baru…",
"repo.editor.new_branch_name_desc": "Nama branch baru\u00e2\u20ac\u00a6",
"repo.editor.cancel": "Membatalkan",
"repo.editor.no_changes_to_show": "Tidak ada perubahan untuk ditampilkan.",
"repo.commits.commits": "Melakukan",
@@ -816,7 +816,7 @@
"repo.settings.title": "Judul",
"repo.settings.deploy_key_content": "Konten",
"repo.settings.branches": "Cabang",
"repo.settings.choose_branch": "Pilih branch…",
"repo.settings.choose_branch": "Pilih branch\u00e2\u20ac\u00a6",
"repo.settings.tags": "Tag",
"repo.diff.browse_source": "Telusuri Sumber",
"repo.diff.parent": "orang tua",
@@ -1315,24 +1315,24 @@
"search.search": "Cari",
"search.fuzzy_tooltip": "Include results that closely match the search term",
"search.regexp": "Regexp",
"search.repo_kind": "Search repos",
"search.user_kind": "Search users",
"search.org_kind": "Search orgs",
"search.team_kind": "Search teams",
"search.code_kind": "Search code",
"search.package_kind": "Search packages",
"search.project_kind": "Search projects",
"search.branch_kind": "Search branches",
"search.tag_kind": "Search tags",
"search.commit_kind": "Search commits",
"search.runner_kind": "Search runners",
"search.issue_kind": "Search issues",
"search.pull_kind": "Search pull requests",
"search.repo_kind": "Search repos\u2026",
"search.user_kind": "Search users\u2026",
"search.org_kind": "Search orgs\u2026",
"search.team_kind": "Search teams\u2026",
"search.code_kind": "Search code\u2026",
"search.package_kind": "Search packages\u2026",
"search.project_kind": "Search projects\u2026",
"search.branch_kind": "Search branches\u2026",
"search.tag_kind": "Search tags\u2026",
"search.commit_kind": "Search commits\u2026",
"search.runner_kind": "Search runners\u2026",
"search.issue_kind": "Search issues\u2026",
"search.pull_kind": "Search pull requests\u2026",
"editor.buttons.strikethrough.tooltip": "Add strikethrough text",
"filter.string.asc": "AZ",
"filter.string.desc": "ZA",
"filter.string.asc": "A\u2013Z",
"filter.string.desc": "Z\u2013A",
"install.install": "Installation",
"install.installing_desc": "Installing now, please wait",
"install.installing_desc": "Installing now, please wait\u2026",
"install.host": "Host",
"install.db_schema": "Schema",
"install.ssl_mode": "SSL",
@@ -1409,7 +1409,7 @@
"repo.readme": "README",
"repo.mirror_address_url_invalid": "The provided URL is invalid. Make sure all components of the URL are escaped correctly.",
"repo.mirror_lfs_endpoint_desc": "Sync will attempt to use the clone URL to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">determine the LFS server</a>. You can also specify a custom endpoint if the repository LFS data is stored somewhere else.",
"repo.adopt_search": "Enter username to search for unadopted repositories (leave blank to find all)",
"repo.adopt_search": "Enter username to search for unadopted repositories\u2026 (leave blank to find all)",
"repo.desc.sha256": "SHA256",
"repo.template.webhooks": "Webhooks",
"repo.archive.title": "This repo is archived. You can view files and clone it. You cannot open issues or pull requests or push a commit.",
@@ -1419,7 +1419,7 @@
"repo.migrate_items_labels": "Labels",
"repo.migrate.github_token_desc": "You can put one or more tokens here, separated by commas, to make migrating faster by circumventing the GitHub API rate limit. WARNING: Abusing this feature may violate the service provider's policy and may lead to getting your account(s) blocked.",
"repo.migrate.permission_denied_blocked": "You cannot import from disallowed hosts. Please ask the admin to check ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS settings.",
"repo.migrate.migrating": "Migrating from <b>%s</b>",
"repo.migrate.migrating": "Migrating from <b>%s</b>\u2026",
"repo.migrate.codecommit.aws_access_key_id": "AWS Access Key ID",
"repo.migrate.codecommit.aws_secret_access_key": "AWS Secret Access Key",
"repo.migration_status": "Migration status",
@@ -1468,13 +1468,13 @@
"repo.issues.review.review": "Review",
"repo.issues.assignee.error": "Not all assignees were added, due to an unexpected error.",
"repo.compare.title": "Comparing changes",
"repo.compare.description": "Choose two branches or tags to see whats changed or to start a new pull request.",
"repo.compare.description": "Choose two branches or tags to see what\u2019s changed or to start a new pull request.",
"repo.pulls.new.description": "Discuss and review the changes in this comparison with others.",
"repo.pulls.new.already_existed": "A pull request between these branches already exists",
"repo.pulls.edit.already_changed": "Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes.",
"repo.pulls.select_commit_hold_shift_for_range": "Select commit. Hold Shift and click to select a range.",
"repo.pulls.nothing_to_compare_have_tag": "The selected branches/tags are equal.",
"repo.pulls.is_checking": "Checking for merge conflicts",
"repo.pulls.is_checking": "Checking for merge conflicts\u2026",
"repo.pulls.wrong_commit_id": "commit ID must be a commit ID on the target branch",
"repo.pulls.no_merge_not_ready": "This pull request is not ready to be merged. Check review status and status checks.",
"repo.pulls.rebase_merge_pull_request": "Rebase, then fast-forward",
@@ -1492,7 +1492,7 @@
"repo.pulls.status_checks_need_approvals_helper": "The workflow will only run after approval from the repository maintainer.",
"repo.pulls.cmd_instruction_checkout_title": "Checkout",
"repo.pulls.cmd_instruction_merge_warning": "Warning: This operation cannot merge pull request because \"autodetect manual merge\" is not enabled.",
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By\".",
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By\u2026\".",
"repo.signing.wont_sign.error": "There was an error while checking if the commit could be signed.",
"repo.signing.wont_sign.twofa": "You must have two-factor authentication enabled to have commits signed.",
"repo.wiki": "Wiki",
@@ -1548,8 +1548,8 @@
"repo.settings.visibility.private.bullet_two": "May remove the relationship between it and <strong>forks</strong>, <strong>watchers</strong>, and <strong>stars</strong>.",
"repo.settings.unarchive.text": "Unarchiving the repo will restore its ability to receive commits and pushes, as well as new issues and pull requests.",
"repo.settings.lfs": "LFS",
"repo.settings.lfs_lock_path": "Filepath to lock",
"repo.settings.lfs_pointers.found": "Found %d blob pointer(s) %d associated, %d unassociated (%d missing from store)",
"repo.settings.lfs_lock_path": "Filepath to lock\u2026",
"repo.settings.lfs_pointers.found": "Found %d blob pointer(s) \u2014 %d associated, %d unassociated (%d missing from store)",
"repo.settings.lfs_pointers.sha": "Blob SHA",
"repo.settings.lfs_pointers.oid": "OID",
"repo.settings.pages": "Landing Page",
@@ -1607,7 +1607,7 @@
"repo.branch.commits_divergence_from": "Commit divergence: %[1]d behind and %[2]d ahead of %[3]s",
"repo.branch.commits_no_divergence": "The same as branch %[1]s",
"repo.find_file.follow_symlink": "Follow this symlink to where it is pointing at",
"graphs.component_loading": "Loading %s",
"graphs.component_loading": "Loading %s\u2026",
"org.pinned_repos": "Featured Projects",
"org.public_members": "Public Members",
"org.view_all_members": "View all %d members",
@@ -1615,7 +1615,7 @@
"org.settings.email": "Contact Email Address",
"org.settings.change_visibility": "Change Visibility",
"org.settings.change_visibility_notices_1": "If the organization is converted to private, the repository stars will be removed and cannot be restored.",
"org.settings.change_visibility_notices_2": "Non-members will lose access to the organizations repositories if visibility is changed to private.",
"org.settings.change_visibility_notices_2": "Non-members will lose access to the organization\u2019s repositories if visibility is changed to private.",
"org.settings.change_visibility_success": "The visibility of organization %s has been successfully changed.",
"org.settings.visibility_desc": "Change who can view the organization and its repositories.",
"org.settings.rename": "Rename Organization",
@@ -1670,11 +1670,11 @@
"admin.auths.port": "Port",
"admin.auths.ssh_keys_are_verified": "SSH keys in LDAP are considered as verified",
"admin.auths.helo_hostname": "HELO Hostname",
"admin.auths.oauth2_full_name_claim_name": "Full Name Claim Name. (Optional if set, the user's full name will always be synchronized with this claim)",
"admin.auths.oauth2_full_name_claim_name": "Full Name Claim Name. (Optional \u2014 if set, the user's full name will always be synchronized with this claim)",
"admin.auths.oauth2_ssh_public_key_claim_name": "SSH Public Key Claim Name",
"admin.auths.oauth2_admin_group": "Group Claim value for administrator users. (Optional requires claim name above)",
"admin.auths.oauth2_restricted_group": "Group Claim value for restricted users. (Optional requires claim name above)",
"admin.auths.oauth2_map_group_to_team": "Map claimed groups to Organization teams. (Optional requires claim name above)",
"admin.auths.oauth2_admin_group": "Group Claim value for administrator users. (Optional \u2014 requires claim name above)",
"admin.auths.oauth2_restricted_group": "Group Claim value for restricted users. (Optional \u2014 requires claim name above)",
"admin.auths.oauth2_map_group_to_team": "Map claimed groups to Organization teams. (Optional \u2014 requires claim name above)",
"admin.auths.sspi_auto_create_users_helper": "Allow SSPI auth method to automatically create new accounts for users that log in for the first time",
"admin.auths.sspi_strip_domain_names_helper": "If checked, domain names will be removed from logon names (e.g. \"DOMAIN\\user\" and \"user@example.org\" both will become just \"user\").",
"admin.auths.sspi_separator_replacement_helper": "The character to use to replace the separators of down-level logon names (e.g. the \\ in \"DOMAIN\\user\") and user principal names (e.g. the @ in \"user@example.org\").",
@@ -1784,7 +1784,7 @@
"actions.general.collaborative_owner_not_exist": "The collaborative owner does not exist.",
"actions.general.remove_collaborative_owner": "Remove Collaborative Owner",
"actions.general.remove_collaborative_owner_desc": "Removing a collaborative owner will prevent the repositories of the owner from accessing the actions in this repository. Continue?",
"git.filemode.changed_filemode": "%[1]s %[2]s",
"git.filemode.changed_filemode": "%[1]s \u2192 %[2]s",
"org.pinned_repos_empty_title": "Showcase your best work",
"org.pinned_repos_empty_desc": "Pin up to 6 repositories to highlight your organization's most important projects.",
"org.settings.pinned.manage": "Manage Pins",
@@ -1954,6 +1954,12 @@
"repo.settings.pages.nav_show_repository": "Tampilkan tautan repositori (tombol Lihat kode sumber)",
"repo.settings.pages.nav_show_releases": "Tampilkan tautan rilis",
"repo.settings.pages.nav_show_issues": "Tampilkan tautan isu",
"repo.settings.pages.section_labels": "Label bagian",
"repo.settings.pages.section_labels_desc": "Sesuaikan judul yang ditampilkan di halaman arahan untuk setiap bagian",
"repo.settings.pages.label_value_props": "Judul proposisi nilai",
"repo.settings.pages.label_value_props_help": "Judul untuk bagian proposisi nilai (mis., \"Mengapa memilih kami\")",
"repo.settings.pages.label_features": "Judul fitur",
"repo.settings.pages.label_features_help": "Judul untuk bagian fitur (mis., \"Kemampuan\")",
"repo.settings.pages.blog_section": "Bagian blog",
"repo.settings.pages.blog_enabled_desc": "Tampilkan postingan blog terbaru di halaman arahan",
"repo.settings.pages.blog_headline": "Judul blog",
@@ -1974,6 +1980,9 @@
"repo.settings.pages.translations": "Terjemahan",
"repo.settings.pages.ai_translate": "Terjemahan AI",
"repo.settings.pages.ai_translate_success": "Terjemahan telah berhasil dibuat oleh AI. Tinjau dan edit sesuai kebutuhan.",
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
"repo.settings.pages.delete_translation": "Hapus",
"repo.settings.pages.save_translation": "Simpan terjemahan",
"repo.settings.pages.translation_saved": "Terjemahan berhasil disimpan.",
@@ -1986,6 +1995,43 @@
"repo.settings.pages.trans_cta_headline": "Judul bagian CTA",
"repo.settings.pages.trans_cta_subheadline": "Subjudul bagian CTA",
"repo.settings.pages.trans_cta_button": "Label tombol CTA",
"repo.settings.pages.trans_section_brand": "Brand",
"repo.settings.pages.trans_section_hero": "Hero",
"repo.settings.pages.trans_section_stats": "Stats",
"repo.settings.pages.trans_section_value_props": "Value Propositions",
"repo.settings.pages.trans_section_features": "Features",
"repo.settings.pages.trans_section_testimonials": "Testimonials",
"repo.settings.pages.trans_section_pricing": "Pricing",
"repo.settings.pages.trans_section_cta": "Call to Action",
"repo.settings.pages.trans_section_blog": "Blog",
"repo.settings.pages.trans_section_gallery": "Gallery",
"repo.settings.pages.trans_section_comparison": "Comparison",
"repo.settings.pages.trans_section_footer": "Footer",
"repo.settings.pages.trans_section_seo": "SEO",
"repo.settings.pages.trans_brand_name": "Brand Name",
"repo.settings.pages.trans_brand_tagline": "Tagline",
"repo.settings.pages.trans_stat_value": "Value",
"repo.settings.pages.trans_stat_label": "Label",
"repo.settings.pages.trans_title": "Title",
"repo.settings.pages.trans_description": "Description",
"repo.settings.pages.trans_quote": "Quote",
"repo.settings.pages.trans_role": "Role",
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
"repo.settings.pages.trans_plan_name": "Plan Name",
"repo.settings.pages.trans_plan_period": "Period",
"repo.settings.pages.trans_plan_cta": "Plan Button",
"repo.settings.pages.trans_blog_headline": "Blog Headline",
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
"repo.settings.pages.trans_blog_cta": "Blog Button",
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
"repo.settings.pages.trans_footer_copyright": "Copyright",
"repo.settings.pages.trans_footer_link": "Link Label",
"repo.settings.pages.trans_seo_title": "SEO Title",
"repo.settings.pages.trans_seo_description": "SEO Description",
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
@@ -2271,5 +2317,18 @@
"repo.view_file": "View File",
"actions.runners.waiting_jobs": "Pekerjaan Menunggu",
"actions.runners.back_to_runners": "Kembali ke Runner",
"actions.runners.no_waiting_jobs": "Tidak ada pekerjaan yang menunggu untuk label ini"
"actions.runners.no_waiting_jobs": "Tidak ada pekerjaan yang menunggu untuk label ini",
"repo.settings.pages.cross_promote_section": "Bagian promosi silang",
"repo.settings.pages.cross_promote_enabled_desc": "Tampilkan repositori promosi silang di halaman arahan",
"repo.settings.pages.cross_promote_headline": "Judul bagian",
"repo.settings.pages.cross_promote_subheadline": "Subjudul bagian",
"repo.settings.pages.cross_promote_help": "Konfigurasikan repositori di Pengaturan > Promosi Silang.",
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files": "Kecualikan file dan folder tersembunyi",
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files_desc": "Jangan dorong file/folder yang dimulai dengan \".\" atau ditandai sebagai tersembunyi ke mirror jarak jauh",
"repo.settings.pages.value_props_headline": "Judul bagian",
"repo.settings.pages.value_props_headline_help": "Judul besar untuk bagian proposisi nilai. Label kecil di atasnya diatur di Label bagian.",
"repo.settings.pages.value_props_subheadline": "Subjudul bagian",
"repo.settings.pages.features_headline": "Judul bagian",
"repo.settings.pages.features_headline_help": "Judul besar untuk bagian fitur. Label kecil di atasnya diatur di Label bagian.",
"repo.settings.pages.features_subheadline": "Subjudul bagian"
}

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -14,8 +14,8 @@
"language": "Taal",
"notifications": "Meldingen",
"active_stopwatch": "Actieve Tijd Tracker",
"create_new": "Maken…",
"user_profile_and_more": "Profiel en instellingen…",
"create_new": "Maken\u00e2\u20ac\u00a6",
"user_profile_and_more": "Profiel en instellingen\u00e2\u20ac\u00a6",
"signed_in_as": "Aangemeld als",
"toc": "Inhoudsopgave",
"licenses": "Licenties",
@@ -30,7 +30,7 @@
"passcode": "PIN",
"webauthn_insert_key": "Voer uw beveiligingssleutel in",
"webauthn_sign_in": "Druk op de knop van uw beveiligingssleutel. Als uw beveiligingssleutel geen knop heeft, voeg deze dan opnieuw in.",
"webauthn_press_button": "Druk alstublieft op de knop van uw beveiligingssleutel…",
"webauthn_press_button": "Druk alstublieft op de knop van uw beveiligingssleutel\u00e2\u20ac\u00a6",
"webauthn_use_twofa": "Gebruik een twee-factor code van uw telefoon",
"webauthn_error": "Kon uw beveiligingssleutel niet lezen.",
"webauthn_unsupported_browser": "Uw browser ondersteunt momenteel geen WebAuthn.",
@@ -73,11 +73,11 @@
"edit": "Bewerken",
"enabled": "Ingeschakeld",
"disabled": "Uitgeschakeld",
"copy": "Kopiëren",
"copy": "Kopi\u00ebren",
"copy_url": "Kopieer URL",
"copy_branch": "Kopieer branchnaam",
"copy_success": "Gekopieerd!",
"copy_error": "Kopiëren mislukt",
"copy_error": "Kopi\u00c3\u00abren mislukt",
"write": "Schrijven",
"preview": "Voorbeeld",
"loading": "Laden...",
@@ -96,12 +96,12 @@
"error.occurred": "Er is een fout opgetreden",
"error.not_found": "Het doel kon niet worden gevonden.",
"error.network_error": "Netwerk fout",
"startpage.app_desc": "Geïntegreerd in je workflow",
"startpage.app_desc": "Ge\u00c3\u00afntegreerd in je workflow",
"startpage.install": "Overal Implementeren",
"startpage.lightweight": "Razendsnel",
"startpage.lightweight_desc": "Minimale voetafdruk, maximale prestaties. GitCaddy draait efficiënt op alles, van Raspberry Pi tot enterprise servers.",
"startpage.lightweight_desc": "Minimale voetafdruk, maximale prestaties. GitCaddy draait effici\u00c3\u00abnt op alles, van Raspberry Pi tot enterprise servers.",
"install.install": "Installatie",
"install.title": "Initiële configuratie",
"install.title": "Initi\u00c3\u00able configuratie",
"install.docker_helper": "Als je GitCaddy draait in Docker, Lees eerst de <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">documentatie</a> voordat je een instelling aanpast.",
"install.require_db_desc": "GitCaddy vereist MySQL, PostgreSQL, MSSQL, SQLite3 of TiDB (MySQL protocol).",
"install.db_title": "Database-instellingen",
@@ -162,7 +162,7 @@
"install.enable_captcha": "Registratie CAPTCHA inschakelen",
"install.enable_captcha_popup": "Vereis captcha validatie voor zelf-registratie van gebruiker.",
"install.require_sign_in_view": "Vereis inloggen om pagina's te kunnen bekijken",
"install.admin_setting_desc": "Het creëren van een administrator-account is optioneel. De eerste geregistreerde gebruiker wordt automatisch de beheerder.",
"install.admin_setting_desc": "Het cre\u00c3\u00abren van een administrator-account is optioneel. De eerste geregistreerde gebruiker wordt automatisch de beheerder.",
"install.admin_title": "Instellingen beheerdersaccount",
"install.admin_name": "Admin gebruikersnaam",
"install.admin_password": "Wachtwoord",
@@ -170,7 +170,7 @@
"install.admin_email": "E-mailadres",
"install.install_btn_confirm": "Installeer GitCaddy",
"install.test_git_failed": "Git test niet gelukt: 'git' commando %v",
"install.sqlite3_not_available": "Deze GitCaddy-versie biedt geen ondersteuning voor SQLite3. Download de officiële build van %s (niet de versie van de 'gobuild').",
"install.sqlite3_not_available": "Deze GitCaddy-versie biedt geen ondersteuning voor SQLite3. Download de offici\u00c3\u00able build van %s (niet de versie van de 'gobuild').",
"install.invalid_db_setting": "De database instelling zijn niet correct: %v",
"install.invalid_repo_path": "Het pad van de hoofdmap van de repository is ongeldig: %v",
"install.invalid_app_data_path": "Ongeldig app-gegevenspad: %v",
@@ -192,7 +192,7 @@
"home.uname_holder": "Gebruikersnaam of e-mailadres",
"home.password_holder": "Wachtwoord",
"home.switch_dashboard_context": "Wissel voorpaginacontext",
"home.show_more_repos": "Toon meer repositories…",
"home.show_more_repos": "Toon meer repositories\u00e2\u20ac\u00a6",
"home.collaborative_repos": "Gedeelde repositories",
"home.my_orgs": "Mijn organisaties",
"home.my_mirrors": "Mijn spiegels",
@@ -204,16 +204,16 @@
"home.show_both_archived_unarchived": "Toont zowel gearchiveerd als niet-gearchiveerd",
"home.show_only_archived": "Toon alleen gearchiveerd",
"home.show_only_unarchived": "Toon alleen niet gearchiveerd",
"home.show_private": "Privé",
"home.show_both_private_public": "Toon zowel openbaar als privé",
"home.show_only_private": "Toon alleen privé",
"home.show_private": "Priv\u00c3\u00a9",
"home.show_both_private_public": "Toon zowel openbaar als priv\u00c3\u00a9",
"home.show_only_private": "Toon alleen priv\u00c3\u00a9",
"home.show_only_public": "Toon alleen opbenbaar",
"home.issues.in_your_repos": "In uw repositories",
"explore.users": "Gebruikers",
"explore.organizations": "Organisaties",
"explore.packages": "Pakketten",
"explore.packages.empty.description": "Er zijn nog geen openbare of globale pakketten beschikbaar.",
"explore.code_last_indexed_at": "Laatst geïndexeerd %s",
"explore.code_last_indexed_at": "Laatst ge\u00c3\u00afndexeerd %s",
"auth.create_new_account": "Account registreren",
"auth.disable_register_prompt": "Registratie is uitgeschakeld. Neem alstublieft contact op met de pagina beheerder.",
"auth.disable_register_mail": "E-mailbevestiging voor registratie is uitgeschakeld.",
@@ -236,7 +236,7 @@
"auth.reset_password_helper": "Account herstellen",
"auth.password_too_short": "De lengte van uw wachtwoord moet tenminste %d karakters zijn.",
"auth.non_local_account": "Non-lokale gebruikers mogen hun wachtwoord niet wijzigen via de webinterface.",
"auth.verify": "Verifiëren",
"auth.verify": "Verifi\u00c3\u00abren",
"auth.scratch_code": "Eenmalige code",
"auth.use_scratch_code": "Gebruik een eenmalige code",
"auth.twofa_scratch_used": "Je hebt je eenmalige code gebruikt. Je wordt omgeleid naar de tweeledige-authenticatie instellingen pagina zodat je de inschrijving van het apparaat kan verwijderen of een nieuwe eenmalige code kan genereren.",
@@ -265,7 +265,7 @@
"auth.sspi_auth_failed": "SSPI-authenticatie mislukt",
"auth.password_pwned_err": "Kan het verzoek om HaveIBeenPwned niet voltooien",
"mail.view_it_on": "Bekijk het op %s",
"mail.link_not_working_do_paste": "Werkt dit niet? Probeer het te kopiëren en te plakken naar uw browser.",
"mail.link_not_working_do_paste": "Werkt dit niet? Probeer het te kopi\u00c3\u00abren en te plakken naar uw browser.",
"mail.hi_user_x": "Hoi <b>%s</b>,",
"mail.activate_account": "Activeer uw account",
"mail.activate_account.title": "%s, activeer alstublieft uw account",
@@ -344,36 +344,36 @@
"form.username_been_taken": "Deze naam is al in gebruik.",
"form.username_change_not_local_user": "Niet-lokale gebruikers mogen hun gebruikersnaam niet wijzigen.",
"form.repo_name_been_taken": "De repository-naam wordt al gebruikt.",
"form.repository_force_private": "Forceer privé is ingeschakeld: privé repositories kunnen niet openbaar worden gemaakt.",
"form.repository_force_private": "Forceer priv\u00c3\u00a9 is ingeschakeld: priv\u00c3\u00a9 repositories kunnen niet openbaar worden gemaakt.",
"form.repository_files_already_exist": "Er bestaan al bestanden voor deze repository. Neem contact op met de systeembeheerder.",
"form.repository_files_already_exist.delete": "Er bestaan al bestanden voor deze repository. U moet deze verwijderen.",
"form.repository_files_already_exist.adopt_or_delete": "Er bestaan al bestanden voor deze repository. Adopteer of verwijder deze.",
"form.visit_rate_limit": "Bezoeklimiet op afstand gerichter.",
"form.org_name_been_taken": "Naam van de organisatie wordt al gebruikt.",
"form.team_name_been_taken": "De teamnaam is al in gebruik.",
"form.team_no_units_error": "Toegang verlenen tot ten minste één repository sectie.",
"form.team_no_units_error": "Toegang verlenen tot ten minste \u00c3\u00a9\u00c3\u00a9n repository sectie.",
"form.email_been_used": "Het emailadres is al in gebruik.",
"form.email_invalid": "Het e-mailadres is ongeldig.",
"form.username_password_incorrect": "Gebruikersnaam of wachtwoord is onjuist.",
"form.password_complexity": "Wachtwoord voldoet niet aan complexiteit eisen:",
"form.password_lowercase_one": "Minstens één kleine letter",
"form.password_uppercase_one": "Minstens één hoofdletter",
"form.password_digit_one": "Minstens één cijfer",
"form.password_special_one": "Minstens één speciaal teken (interpunctie, haakjes, aanhalingstekens, etc.)",
"form.password_lowercase_one": "Minstens \u00c3\u00a9\u00c3\u00a9n kleine letter",
"form.password_uppercase_one": "Minstens \u00c3\u00a9\u00c3\u00a9n hoofdletter",
"form.password_digit_one": "Minstens \u00c3\u00a9\u00c3\u00a9n cijfer",
"form.password_special_one": "Minstens \u00c3\u00a9\u00c3\u00a9n speciaal teken (interpunctie, haakjes, aanhalingstekens, etc.)",
"form.enterred_invalid_repo_name": "De repository-naam die u hebt ingevoerd is niet correct.",
"form.enterred_invalid_org_name": "De organizatienaam die u hebt ingevoerd is niet correct.",
"form.enterred_invalid_owner_name": "De nieuwe eigenaarnaam is niet geldig.",
"form.enterred_invalid_password": "Het ingevoerde wachtwoord is onjuist.",
"form.user_not_exist": "De gebruiker bestaat niet.",
"form.team_not_exist": "Dit team bestaat niet.",
"form.last_org_owner": "Je kunt de laatste eigenaar van een organisatie niet verwijderen. Er moet er minimaal één eigenaar in een organisatie zitten.",
"form.last_org_owner": "Je kunt de laatste eigenaar van een organisatie niet verwijderen. Er moet er minimaal \u00c3\u00a9\u00c3\u00a9n eigenaar in een organisatie zitten.",
"form.cannot_add_org_to_team": "Een organisatie kan niet worden toegevoegd als een teamlid.",
"form.invalid_ssh_key": "Kan de SSH-sleutel niet verifiëren: %s",
"form.invalid_gpg_key": "Kan de GPG-sleutel niet verifiëren: %s",
"form.invalid_ssh_key": "Kan de SSH-sleutel niet verifi\u00c3\u00abren: %s",
"form.invalid_gpg_key": "Kan de GPG-sleutel niet verifi\u00c3\u00abren: %s",
"form.invalid_ssh_principal": "Ongeldige verantwoordelijke: %s",
"form.auth_failed": "Verificatie mislukt: %v",
"form.target_branch_not_exist": "Doel branch bestaat niet",
"user.change_avatar": "Wijzig je profielfoto…",
"user.change_avatar": "Wijzig je profielfoto\u00e2\u20ac\u00a6",
"user.repositories": "Repository's",
"user.activity": "Activiteit",
"user.followers": "Volgers",
@@ -430,7 +430,7 @@
"settings.enable_custom_avatar": "Aangepaste avatar inschakelen",
"settings.choose_new_avatar": "Kies een nieuwe avatar",
"settings.delete_current_avatar": "Verwijder huidige avatar",
"settings.uploaded_avatar_not_a_image": "Het geüploade bestand is geen afbeelding.",
"settings.uploaded_avatar_not_a_image": "Het ge\u00c3\u00bcploade bestand is geen afbeelding.",
"settings.update_avatar_success": "Je avatar is bijgewerkt.",
"settings.update_user_avatar_success": "De avatar van de gebruiker is bijgewerkt.",
"settings.change_password": "Wachtwoord bijwerken",
@@ -473,7 +473,7 @@
"settings.add_key": "Sleutel toevoegen",
"settings.ssh_desc": "Deze publieke SSH sleutels worden geassocieerd met uw account. De bijbehorende private sleutels geven volledige toegang toe tot je repositories.",
"settings.principal_desc": "Deze SSH-certificaatverantwoordelijken zijn gekoppeld aan uw account en geven volledige toegang tot uw repositories.",
"settings.gpg_desc": "Deze publieke GPG-sleutels zijn verbonden met je account. Houd je privé-sleutels veilig, omdat hiermee commits kunnen worden ondertekend.",
"settings.gpg_desc": "Deze publieke GPG-sleutels zijn verbonden met je account. Houd je priv\u00c3\u00a9-sleutels veilig, omdat hiermee commits kunnen worden ondertekend.",
"settings.ssh_helper": "<strong>Weet u niet hoe?</strong> Lees dan onze handleiding voor het <a href=\"%s\"> genereren van SSH sleutels</a> of voor <a href=\"%s\"> algemene SSH</a> problemen.",
"settings.gpg_helper": "<strong>Hulp nodig?</strong> Neem een kijkje op de GitHub handleiding <a href=\"%s\">over GPG</a>.",
"settings.add_new_key": "SSH sleutel toevoegen",
@@ -489,15 +489,15 @@
"settings.gpg_key_matched_identities": "Overeenkomende identiteiten:",
"settings.gpg_key_matched_identities_long": "De ingesloten identiteiten in deze sleutel komen overeen met de geactiveerde e-mailadressen voor deze gebruiker. Commits die overeenkomen met deze e-mailadressen kunnen worden geverifieerd met deze sleutel.",
"settings.gpg_key_verified": "Geverifieerde sleutel",
"settings.gpg_key_verified_long": "Sleutel is geverifieerd met een token en kan worden gebruikt om commits te verifiëren die overeenkomen met alle geactiveerde e-mailadressen voor deze gebruiker naast de bijbehorende identiteiten voor deze sleutel.",
"settings.gpg_key_verify": "Verifiëren",
"settings.gpg_key_verified_long": "Sleutel is geverifieerd met een token en kan worden gebruikt om commits te verifi\u00c3\u00abren die overeenkomen met alle geactiveerde e-mailadressen voor deze gebruiker naast de bijbehorende identiteiten voor deze sleutel.",
"settings.gpg_key_verify": "Verifi\u00c3\u00abren",
"settings.gpg_token_required": "U moet een handtekening opgeven voor de onderstaande token",
"settings.gpg_token_help": "U kunt een handtekening genereren met:",
"settings.gpg_token_signature": "Gepantserde GPG-handtekening",
"settings.key_signature_gpg_placeholder": "Begint met '-----BEGIN PGP SIGNATURE-----'",
"settings.ssh_key_verified": "Geverifieerde sleutel",
"settings.ssh_key_verified_long": "Sleutel is geverifieerd met een token en kan worden gebruikt om commits te verifiëren die overeenkomen met alle geactiveerde e-mailadressen voor deze gebruiker.",
"settings.ssh_key_verify": "Verifiëren",
"settings.ssh_key_verified_long": "Sleutel is geverifieerd met een token en kan worden gebruikt om commits te verifi\u00c3\u00abren die overeenkomen met alle geactiveerde e-mailadressen voor deze gebruiker.",
"settings.ssh_key_verify": "Verifi\u00c3\u00abren",
"settings.ssh_token_required": "U moet een handtekening opgeven voor het onderstaande token",
"settings.ssh_token_help": "U kunt een handtekening genereren door het volgende:",
"settings.ssh_token_signature": "Gepantserde SSH handtekening",
@@ -597,7 +597,7 @@
"settings.visibility": "Gebruiker zichtbaarheid",
"settings.visibility.public": "Openbaar",
"settings.visibility.limited": "Beperkt",
"settings.visibility.private": "Privé",
"settings.visibility.private": "Priv\u00c3\u00a9",
"repo.owner": "Eigenaar",
"repo.owner_helper": "Sommige organisaties kunnen niet worden weergegeven in de dropdown vanwege een limiet op het maximale aantal repositories.",
"repo.repo_name": "Naam van repository",
@@ -608,7 +608,7 @@
"repo.template_description": "Sjabloon repositories laten gebruikers nieuwe repositories genereren met dezelfde directory structuur, bestanden en optionele instellingen.",
"repo.visibility": "Zichtbaarheid",
"repo.visibility_description": "Alleen de eigenaar of de organisatielid kan het zien als ze rechten hebben.",
"repo.visibility_helper_forced": "De sitebeheerder verplicht alle repositories om privé te zijn.",
"repo.visibility_helper_forced": "De sitebeheerder verplicht alle repositories om priv\u00c3\u00a9 te zijn.",
"repo.visibility_fork_helper": "(Verandering van deze waarde zal van invloed zijn op alle forks)",
"repo.clone_helper": "Heb je hulp nodig om te clonen? Bekijk dan de <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">handleiding</a>.",
"repo.fork_repo": "Repository forken",
@@ -671,18 +671,18 @@
"repo.blame_prior": "Bekijk de schuld voorafgaand aan deze verandering",
"repo.transfer.accept": "Accepteer overdracht",
"repo.transfer.reject": "Overdracht afwijzen",
"repo.desc.private": "Privé",
"repo.desc.private": "Priv\u00c3\u00a9",
"repo.desc.public": "Openbaar",
"repo.desc.template": "Sjabloon",
"repo.desc.internal": "Interne",
"repo.desc.archived": "Gearchiveerd",
"repo.template.items": "Sjabloon items",
"repo.template.git_content": "Git inhoud (standaard Branch)",
"repo.template.git_hooks_tooltip": "Je bent momenteel niet in staat om Git Hooks één keer te wijzigen of te verwijderen. Selecteer deze optie alleen als je de sjabloonrepository vertrouwt.",
"repo.template.git_hooks_tooltip": "Je bent momenteel niet in staat om Git Hooks \u00c3\u00a9\u00c3\u00a9n keer te wijzigen of te verwijderen. Selecteer deze optie alleen als je de sjabloonrepository vertrouwt.",
"repo.template.topics": "Onderwerpen",
"repo.template.avatar": "Profielfoto",
"repo.template.issue_labels": "Issuelabels",
"repo.template.one_item": "Moet ten minste één sjabloon selecteren",
"repo.template.one_item": "Moet ten minste \u00c3\u00a9\u00c3\u00a9n sjabloon selecteren",
"repo.template.invalid": "Moet een sjabloon repository selecteren",
"repo.archive.issue.nocomment": "Deze repo is gearchiveerd. U kunt niet reageren op problemen.",
"repo.archive.pull.nocomment": "Deze repo is gearchiveerd. U kunt niet reageren op pull requests.",
@@ -773,7 +773,7 @@
"repo.invisible_runes_line": "Deze lijn heeft onzichtbare unicode karakters",
"repo.ambiguous_runes_line": "Deze lijn heeft dubbelzinnige unicode karakters",
"repo.unescape_control_characters": "Onescape",
"repo.file_copy_permalink": "Permalink kopiëren",
"repo.file_copy_permalink": "Permalink kopi\u00c3\u00abren",
"repo.view_git_blame": "Bekijk Git Blame",
"repo.video_not_supported_in_browser": "Je browser ondersteunt de HTML5 'video'-tag niet.",
"repo.audio_not_supported_in_browser": "Je browser ondersteunt de HTML5 'audio'-tag niet.",
@@ -799,7 +799,7 @@
"repo.editor.fork_before_edit": "Je moet deze repository forken om veranderingen te maken of voor te stellen.",
"repo.editor.delete_this_file": "Verwijder bestand",
"repo.editor.must_have_write_access": "U moet schrijftoegang hebben om aanpassingen te maken of voor te stellen in dit bestand.",
"repo.editor.name_your_file": "Bestandsnaam…",
"repo.editor.name_your_file": "Bestandsnaam\u00e2\u20ac\u00a6",
"repo.editor.filename_help": "Voeg een map toe door zijn naam te typen, gevolgd door een slash ('/'). Verwijder een map door op backspace te drukken aan het begin van het tekstveld.",
"repo.editor.or": "of",
"repo.editor.cancel_lower": "Annuleer",
@@ -809,13 +809,13 @@
"repo.editor.patching": "Patchen:",
"repo.editor.new_patch": "Nieuwe Patch",
"repo.editor.commit_message": "Commit-bericht",
"repo.editor.commit_message_desc": "Voeg een optionele uitgebreide omschrijving toe…",
"repo.editor.commit_message_desc": "Voeg een optionele uitgebreide omschrijving toe\u00e2\u20ac\u00a6",
"repo.editor.signoff_desc": "Voeg een Signed-off-by toe aan het einde van het commit logbericht.",
"repo.editor.commit_directly_to_this_branch": "Commit direct naar de branch '<strong class=\"branch-name\">%s</strong>'.",
"repo.editor.create_new_branch": "Maak een <strong>nieuwe branch</strong> voor deze commit en start van een pull-aanvraag.",
"repo.editor.create_new_branch_np": "Maak een <strong>nieuwe branch</strong> voor deze commit.",
"repo.editor.propose_file_change": "Stel bestandswijziging voor",
"repo.editor.new_branch_name_desc": "Nieuwe branch naam…",
"repo.editor.new_branch_name_desc": "Nieuwe branch naam\u00e2\u20ac\u00a6",
"repo.editor.cancel": "Annuleer",
"repo.editor.filename_cannot_be_empty": "Bestandsnaam mag niet leeg zijn.",
"repo.editor.file_changed_while_editing": "De bestandsinhoud is veranderd sinds je bent begonnen met bewerken. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">Klik hier</a> om ze te zien, of <strong>commit de veranderingen opnieuw</strong> om ze te overschrijven.",
@@ -825,7 +825,7 @@
"repo.editor.push_rejected_no_message": "De wijziging is afgewezen door de server zonder bericht. Controleer de Git Hooks alsjeblieft.",
"repo.editor.push_rejected": "De wijziging is afgewezen door de server. Controleer Controleer de Git Hooks alsjeblieft.",
"repo.editor.push_rejected_summary": "Volledig afwijzingsbericht:",
"repo.editor.add_subdir": "Een map toevoegen…",
"repo.editor.add_subdir": "Een map toevoegen\u00e2\u20ac\u00a6",
"repo.editor.no_commit_to_branch": "Kan niet rechtstreeks naar branch committen omdat:",
"repo.editor.user_no_push_to_branch": "Gebruiker kan niet pushen naar branch",
"repo.editor.require_signed_commit": "Branch vereist een ondertekende commit",
@@ -857,7 +857,7 @@
"repo.projects.create": "Project aanmaken",
"repo.projects.title": "Titel",
"repo.projects.new": "Nieuw project",
"repo.projects.new_subheader": "Coördineer, track en update uw werk op één plek, dus projecten blijven transparant en op schema.",
"repo.projects.new_subheader": "Co\u00c3\u00b6rdineer, track en update uw werk op \u00c3\u00a9\u00c3\u00a9n plek, dus projecten blijven transparant en op schema.",
"repo.projects.deletion": "Project verwijderen",
"repo.projects.deletion_desc": "Als een project wordt verwijdert, wordt deze van alle gerelateerde kwesties verwijderd. Doorgaan?",
"repo.projects.deletion_success": "Het project is verwijderd.",
@@ -971,7 +971,7 @@
"repo.issues.num_comments": "%d opmerkingen",
"repo.issues.commented_at": "reageerde <a href=\"#%s\">%s</a>",
"repo.issues.delete_comment_confirm": "Weet u zeker dat u deze reactie wilt verwijderen?",
"repo.issues.context.copy_link": "Link kopiëren",
"repo.issues.context.copy_link": "Link kopi\u00c3\u00abren",
"repo.issues.context.quote_reply": "Citeer antwoord",
"repo.issues.context.reference_issue": "Verwijs in nieuw issue",
"repo.issues.context.edit": "Bewerken",
@@ -1071,7 +1071,7 @@
"repo.issues.dependency.title": "Afhankelijkheden",
"repo.issues.dependency.issue_no_dependencies": "Geen afhankelijkheden ingesteld.",
"repo.issues.dependency.pr_no_dependencies": "Geen afhankelijkheden ingesteld.",
"repo.issues.dependency.add": "Voeg afhankelijkheid toe…",
"repo.issues.dependency.add": "Voeg afhankelijkheid toe\u00e2\u20ac\u00a6",
"repo.issues.dependency.cancel": "Annuleer",
"repo.issues.dependency.remove": "Verwijder",
"repo.issues.dependency.remove_info": "Verwijder afhankelijkheid",
@@ -1115,7 +1115,7 @@
"repo.issues.reference_issue.body": "Inhoud",
"repo.issues.content_history.deleted": "verwijderd",
"repo.issues.content_history.edited": "bewerkt",
"repo.issues.content_history.created": "gecreëerd",
"repo.issues.content_history.created": "gecre\u00c3\u00aberd",
"repo.issues.content_history.delete_from_history": "Uit geschiedenis verwijderen",
"repo.issues.content_history.delete_from_history_confirm": "Uit geschiedenis verwijderen?",
"repo.issues.content_history.options": "Opties",
@@ -1251,7 +1251,7 @@
"repo.wiki.back_to_wiki": "Terug naar wiki-pagina",
"repo.wiki.delete_page_button": "Verwijder pagina",
"repo.wiki.page_already_exists": "Er bestaat al een wiki-pagina met deze naam.",
"repo.wiki.pages": "Pagina’s",
"repo.wiki.pages": "Pagina\u00e2\u20ac\u2122s",
"repo.wiki.last_updated": "Laatst bijgewerkt: %s",
"repo.wiki.page_name_desc": "Voer een naam in voor deze Wiki pagina. Sommige speciale namen zijn: 'Home', '_Sidebar' en '_Footer'.",
"repo.activity": "Activiteit",
@@ -1411,9 +1411,9 @@
"repo.settings.discord_icon_url": "Icoon URL",
"repo.settings.event_desc": "Trigger op:",
"repo.settings.event_send_everything": "Alle gebeurtenissen",
"repo.settings.event_choose": "Aangepaste gebeurtenissen…",
"repo.settings.event_choose": "Aangepaste gebeurtenissen\u00e2\u20ac\u00a6",
"repo.settings.event_header_repository": "Repository gebeurtenissen",
"repo.settings.event_create": "Creëer",
"repo.settings.event_create": "Cre\u00c3\u00aber",
"repo.settings.event_create_desc": "Branch, of tag aangemaakt.",
"repo.settings.event_delete": "Verwijder",
"repo.settings.event_delete_desc": "Branch of tag verwijderd.",
@@ -1487,13 +1487,13 @@
"repo.settings.require_signed_commits_desc": "Weiger pushes naar deze branch als deze niet ondertekend of niet verifieerbaar is.",
"repo.settings.protected_branch_deletion_desc": "Branch bescherming uitschakelen zorgt ervoor dat gebruikers met schrijfrechten naar de branch kunnen pushen. Doorgaan?",
"repo.settings.block_rejected_reviews": "Samenvoegen van afgewezen beoordelingen blokkeren",
"repo.settings.block_rejected_reviews_desc": "Samenvoegen zal niet mogelijk zijn wanneer er wijzigingen worden aangevraagd door officiële beoordelaars, zelfs niet als er genoeg goedkeuringen zijn.",
"repo.settings.block_on_official_review_requests": "Blokkeer de samenvoeging van officiële beoordelingsverzoeken",
"repo.settings.block_on_official_review_requests_desc": "Samenvoegen is niet mogelijk wanneer het officiële herzieningsverzoeken heeft, ook al zijn er genoeg goedkeuringen.",
"repo.settings.block_rejected_reviews_desc": "Samenvoegen zal niet mogelijk zijn wanneer er wijzigingen worden aangevraagd door offici\u00c3\u00able beoordelaars, zelfs niet als er genoeg goedkeuringen zijn.",
"repo.settings.block_on_official_review_requests": "Blokkeer de samenvoeging van offici\u00c3\u00able beoordelingsverzoeken",
"repo.settings.block_on_official_review_requests_desc": "Samenvoegen is niet mogelijk wanneer het offici\u00c3\u00able herzieningsverzoeken heeft, ook al zijn er genoeg goedkeuringen.",
"repo.settings.block_outdated_branch": "Samenvoegen blokkeren als pull request verouderd is",
"repo.settings.block_outdated_branch_desc": "Samenvoegen is niet mogelijk als de hoofd branch achter loop op de basis branch.",
"repo.settings.default_branch_desc": "Selecteer een standaard repository branch voor pull requests en code commits:",
"repo.settings.choose_branch": "Kies een branch…",
"repo.settings.choose_branch": "Kies een branch\u00e2\u20ac\u00a6",
"repo.settings.no_protected_branch": "Er zijn geen beschermde branches.",
"repo.settings.edit_protected_branch": "Bewerken",
"repo.settings.protected_branch_required_approvals_min": "Vereiste goedkeuringen kunnen niet negatief zijn.",
@@ -1572,7 +1572,7 @@
"repo.diff.load": "Laad Diff",
"repo.diff.generated": "gegenereerd",
"repo.diff.comment.placeholder": "Opmerking toevoegen",
"repo.diff.comment.add_single_comment": "Één reactie toevoegen",
"repo.diff.comment.add_single_comment": "\u00c3\u2030\u00c3\u00a9n reactie toevoegen",
"repo.diff.comment.add_review_comment": "Voeg commentaar toe",
"repo.diff.comment.start_review": "Review starten",
"repo.diff.comment.reply": "Reageer",
@@ -1600,7 +1600,7 @@
"repo.release.source_code": "Broncode",
"repo.release.tag_name": "Tagnaam",
"repo.release.target": "Doel",
"repo.release.tag_helper": "Kies een bestaande tag, of creëer een nieuwe tag bij publiceren.",
"repo.release.tag_helper": "Kies een bestaande tag, of cre\u00c3\u00aber een nieuwe tag bij publiceren.",
"repo.release.prerelease_desc": "Markeren als voorlopige versie",
"repo.release.prerelease_helper": "Markeer deze release als ongeschikt voor productiedoeleinden.",
"repo.release.cancel": "Annuleren",
@@ -1652,8 +1652,8 @@
"org.settings.visibility": "Zichtbaarheid",
"org.settings.visibility.public": "Publiek",
"org.settings.visibility.limited_shortname": "Beperkt",
"org.settings.visibility.private": "Privé (alleen zichtbaar voor organisatieleden)",
"org.settings.visibility.private_shortname": "Privé",
"org.settings.visibility.private": "Priv\u00c3\u00a9 (alleen zichtbaar voor organisatieleden)",
"org.settings.visibility.private_shortname": "Priv\u00c3\u00a9",
"org.settings.update_settings": "Instellingen bijwerken",
"org.settings.update_avatar_success": "De avatar van de organisatie is aangepast.",
"org.settings.delete": "Verwijder organisatie",
@@ -1807,7 +1807,7 @@
"admin.users.allow_create_organization": "Mag organisaties aanmaken",
"admin.users.update_profile": "Update gebruikers account",
"admin.users.delete_account": "Verwijder gebruikers account",
"admin.users.still_own_repo": "Deze gebruiker is nog steeds eigenaar van één of meerdere repositories. Verwijder of draag eerst deze repositories over.",
"admin.users.still_own_repo": "Deze gebruiker is nog steeds eigenaar van \u00c3\u00a9\u00c3\u00a9n of meerdere repositories. Verwijder of draag eerst deze repositories over.",
"admin.users.still_has_org": "Deze gebruiker is lid van een organisatie. Verwijder de gebruiker eerst uit alle organisaties.",
"admin.users.deletion_success": "De gebruiker is verwijderd.",
"admin.users.list_status_filter.is_active": "Actief",
@@ -2224,24 +2224,24 @@
"search.search": "Zoeken",
"search.fuzzy_tooltip": "Include results that closely match the search term",
"search.regexp": "Regexp",
"search.repo_kind": "Search repos",
"search.user_kind": "Search users",
"search.org_kind": "Search orgs",
"search.team_kind": "Search teams",
"search.code_kind": "Search code",
"search.package_kind": "Search packages",
"search.project_kind": "Search projects",
"search.branch_kind": "Search branches",
"search.tag_kind": "Search tags",
"search.commit_kind": "Search commits",
"search.runner_kind": "Search runners",
"search.issue_kind": "Search issues",
"search.pull_kind": "Search pull requests",
"search.repo_kind": "Search repos\u2026",
"search.user_kind": "Search users\u2026",
"search.org_kind": "Search orgs\u2026",
"search.team_kind": "Search teams\u2026",
"search.code_kind": "Search code\u2026",
"search.package_kind": "Search packages\u2026",
"search.project_kind": "Search projects\u2026",
"search.branch_kind": "Search branches\u2026",
"search.tag_kind": "Search tags\u2026",
"search.commit_kind": "Search commits\u2026",
"search.runner_kind": "Search runners\u2026",
"search.issue_kind": "Search issues\u2026",
"search.pull_kind": "Search pull requests\u2026",
"aria.footer.links": "Links",
"editor.buttons.strikethrough.tooltip": "Add strikethrough text",
"filter.string.asc": "AZ",
"filter.string.desc": "ZA",
"install.installing_desc": "Installing now, please wait",
"filter.string.asc": "A\u2013Z",
"filter.string.desc": "Z\u2013A",
"install.installing_desc": "Installing now, please wait\u2026",
"install.db_schema": "Schema",
"install.ssl_mode": "SSL",
"install.reinstall_confirm_check_1": "The data encrypted by the SECRET_KEY in app.ini may be lost: users may not be able to log in with 2FA/OTP and mirrors may not function correctly. By checking this box, you confirm that the current app.ini file contains the correct SECRET_KEY.",
@@ -2322,7 +2322,7 @@
"repo.mirror_address_url_invalid": "The provided URL is invalid. Make sure all components of the URL are escaped correctly.",
"repo.mirror_lfs_endpoint_desc": "Sync will attempt to use the clone URL to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">determine the LFS server</a>. You can also specify a custom endpoint if the repository LFS data is stored somewhere else.",
"repo.forks": "Forks",
"repo.adopt_search": "Enter username to search for unadopted repositories (leave blank to find all)",
"repo.adopt_search": "Enter username to search for unadopted repositories\u2026 (leave blank to find all)",
"repo.desc.sha256": "SHA256",
"repo.template.webhooks": "Webhooks",
"repo.archive.title": "This repo is archived. You can view files and clone it. You cannot open issues or pull requests or push a commit.",
@@ -2333,7 +2333,7 @@
"repo.migrate_items_releases": "Releases",
"repo.migrate.github_token_desc": "You can put one or more tokens here, separated by commas, to make migrating faster by circumventing the GitHub API rate limit. WARNING: Abusing this feature may violate the service provider's policy and may lead to getting your account(s) blocked.",
"repo.migrate.permission_denied_blocked": "You cannot import from disallowed hosts. Please ask the admin to check ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS settings.",
"repo.migrate.migrating": "Migrating from <b>%s</b>",
"repo.migrate.migrating": "Migrating from <b>%s</b>\u2026",
"repo.migrate.codecommit.aws_access_key_id": "AWS Access Key ID",
"repo.migrate.codecommit.aws_secret_access_key": "AWS Secret Access Key",
"repo.migration_status": "Migration status",
@@ -2388,14 +2388,14 @@
"repo.issues.review.review": "Review",
"repo.issues.assignee.error": "Not all assignees were added, due to an unexpected error.",
"repo.compare.title": "Comparing changes",
"repo.compare.description": "Choose two branches or tags to see whats changed or to start a new pull request.",
"repo.compare.description": "Choose two branches or tags to see what\u2019s changed or to start a new pull request.",
"repo.pulls.new.description": "Discuss and review the changes in this comparison with others.",
"repo.pulls.new.already_existed": "A pull request between these branches already exists",
"repo.pulls.edit.already_changed": "Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes.",
"repo.pulls.select_commit_hold_shift_for_range": "Select commit. Hold Shift and click to select a range.",
"repo.pulls.nothing_to_compare_have_tag": "The selected branches/tags are equal.",
"repo.pulls.tab_commits": "Commits",
"repo.pulls.is_checking": "Checking for merge conflicts",
"repo.pulls.is_checking": "Checking for merge conflicts\u2026",
"repo.pulls.wrong_commit_id": "commit ID must be a commit ID on the target branch",
"repo.pulls.no_merge_not_ready": "This pull request is not ready to be merged. Check review status and status checks.",
"repo.pulls.rebase_merge_pull_request": "Rebase, then fast-forward",
@@ -2413,7 +2413,7 @@
"repo.pulls.status_checks_need_approvals_helper": "The workflow will only run after approval from the repository maintainer.",
"repo.pulls.cmd_instruction_checkout_title": "Checkout",
"repo.pulls.cmd_instruction_merge_warning": "Warning: This operation cannot merge pull request because \"autodetect manual merge\" is not enabled.",
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By\".",
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By\u2026\".",
"repo.signing.wont_sign.error": "There was an error while checking if the commit could be signed.",
"repo.signing.wont_sign.twofa": "You must have two-factor authentication enabled to have commits signed.",
"repo.wiki": "Wiki",
@@ -2474,8 +2474,8 @@
"repo.settings.visibility.private.bullet_two": "May remove the relationship between it and <strong>forks</strong>, <strong>watchers</strong>, and <strong>stars</strong>.",
"repo.settings.unarchive.text": "Unarchiving the repo will restore its ability to receive commits and pushes, as well as new issues and pull requests.",
"repo.settings.lfs": "LFS",
"repo.settings.lfs_lock_path": "Filepath to lock",
"repo.settings.lfs_pointers.found": "Found %d blob pointer(s) %d associated, %d unassociated (%d missing from store)",
"repo.settings.lfs_lock_path": "Filepath to lock\u2026",
"repo.settings.lfs_pointers.found": "Found %d blob pointer(s) \u2014 %d associated, %d unassociated (%d missing from store)",
"repo.settings.lfs_pointers.sha": "Blob SHA",
"repo.settings.lfs_pointers.oid": "OID",
"repo.settings.pages": "Pagina's",
@@ -2535,7 +2535,7 @@
"repo.branch.commits_divergence_from": "Commit divergence: %[1]d behind and %[2]d ahead of %[3]s",
"repo.branch.commits_no_divergence": "The same as branch %[1]s",
"repo.find_file.follow_symlink": "Follow this symlink to where it is pointing at",
"graphs.component_loading": "Loading %s",
"graphs.component_loading": "Loading %s\u2026",
"org.teams": "Teams",
"org.pinned_repos": "Featured Projects",
"org.public_members": "Public Members",
@@ -2544,7 +2544,7 @@
"org.settings.email": "Contact Email Address",
"org.settings.change_visibility": "Change Visibility",
"org.settings.change_visibility_notices_1": "If the organization is converted to private, the repository stars will be removed and cannot be restored.",
"org.settings.change_visibility_notices_2": "Non-members will lose access to the organizations repositories if visibility is changed to private.",
"org.settings.change_visibility_notices_2": "Non-members will lose access to the organization\u2019s repositories if visibility is changed to private.",
"org.settings.change_visibility_success": "The visibility of organization %s has been successfully changed.",
"org.settings.visibility_desc": "Change who can view the organization and its repositories.",
"org.settings.rename": "Rename Organization",
@@ -2660,12 +2660,12 @@
"packages.settings.global_access.disabled": "Globale toegang tot pakket is uitgeschakeld.",
"packages.settings.global_access.error": "Kon globale toegangsinstelling niet bijwerken.",
"packages.visibility": "Zichtbaarheid",
"packages.settings.visibility.private.text": "Dit pakket is momenteel privé. Maak het openbaar om iedereen toegang te geven.",
"packages.settings.visibility.private.button": "Privé maken",
"packages.settings.visibility.private.bullet_title": "U staat op het punt dit pakket privé te maken.",
"packages.settings.visibility.private.text": "Dit pakket is momenteel priv\u00e9. Maak het openbaar om iedereen toegang te geven.",
"packages.settings.visibility.private.button": "Priv\u00e9 maken",
"packages.settings.visibility.private.bullet_title": "U staat op het punt dit pakket priv\u00e9 te maken.",
"packages.settings.visibility.private.bullet_one": "Alleen gebruikers met de juiste rechten kunnen dit pakket openen.",
"packages.settings.visibility.private.success": "Pakket is nu privé.",
"packages.settings.visibility.public.text": "Dit pakket is momenteel openbaar. Maak het privé om de toegang te beperken.",
"packages.settings.visibility.private.success": "Pakket is nu priv\u00e9.",
"packages.settings.visibility.public.text": "Dit pakket is momenteel openbaar. Maak het priv\u00e9 om de toegang te beperken.",
"packages.settings.visibility.public.button": "Openbaar maken",
"packages.settings.visibility.public.bullet_title": "U staat op het punt dit pakket openbaar te maken.",
"packages.settings.visibility.public.bullet_one": "Iedereen kan dit pakket openen en downloaden.",
@@ -2714,7 +2714,7 @@
"actions.general.collaborative_owner_not_exist": "Collaboratieve eigenaar bestaat niet",
"actions.general.remove_collaborative_owner": "Collaboratieve eigenaar verwijderen",
"actions.general.remove_collaborative_owner_desc": "De eigenaar verliest toegang tot runnerbeheer",
"git.filemode.changed_filemode": "%[1]s %[2]s",
"git.filemode.changed_filemode": "%[1]s \u2192 %[2]s",
"org.pinned_repos_empty_title": "Showcase your best work",
"org.pinned_repos_empty_desc": "Pin up to 6 repositories to highlight your organization's most important projects.",
"org.settings.pinned.manage": "Manage Pins",
@@ -2781,7 +2781,7 @@
"admin.config.pinned_org_format_condensed": "Compact",
"admin.config.pinned_org_format_regular": "Normaal",
"admin.config.pinned_org_display_format_help": "Weergaveformaat voor vastgepinde organisaties",
"admin.config.logo_upload_success": "Logo geüpload",
"admin.config.logo_upload_success": "Logo ge\u00fcpload",
"admin.config.logo_url_success": "Logo-URL ingesteld",
"admin.config.logo_reset_success": "Logo gereset",
"admin.config.logo_invalid_type": "Ongeldig logo-bestandstype",
@@ -2792,7 +2792,7 @@
"admin.config.reset_icon": "Pictogram resetten",
"admin.config.current_icon": "Huidig pictogram",
"admin.config.icon_url": "Pictogram-URL",
"admin.config.icon_upload_success": "Pictogram geüpload",
"admin.config.icon_upload_success": "Pictogram ge\u00fcpload",
"admin.config.icon_url_success": "Pictogram-URL ingesteld",
"admin.config.icon_reset_success": "Pictogram gereset",
"admin.config.icon_invalid_type": "Ongeldig bestandstype. Toegestaan: SVG, PNG, ICO",
@@ -2884,6 +2884,12 @@
"repo.settings.pages.nav_show_repository": "Repository-link tonen (Broncode bekijken-knop)",
"repo.settings.pages.nav_show_releases": "Releases-link tonen",
"repo.settings.pages.nav_show_issues": "Issues-link tonen",
"repo.settings.pages.section_labels": "Sectielabels",
"repo.settings.pages.section_labels_desc": "Pas de koppen aan die op uw landingspagina worden weergegeven voor elke sectie",
"repo.settings.pages.label_value_props": "Kop waardeproposities",
"repo.settings.pages.label_value_props_help": "Kop voor de sectie waardeproposities (bijv., \"Waarom voor ons kiezen\")",
"repo.settings.pages.label_features": "Kop functies",
"repo.settings.pages.label_features_help": "Kop voor de sectie functies (bijv., \"Mogelijkheden\")",
"repo.settings.pages.blog_section": "Blogsectie",
"repo.settings.pages.blog_enabled_desc": "Toon recente blogberichten op de landingspagina",
"repo.settings.pages.blog_headline": "Blogtitel",
@@ -2904,6 +2910,9 @@
"repo.settings.pages.translations": "Vertalingen",
"repo.settings.pages.ai_translate": "AI-vertaling",
"repo.settings.pages.ai_translate_success": "Vertaling is succesvol gegenereerd door AI. Controleer en bewerk indien nodig.",
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
"repo.settings.pages.delete_translation": "Verwijderen",
"repo.settings.pages.save_translation": "Vertaling opslaan",
"repo.settings.pages.translation_saved": "Vertaling succesvol opgeslagen.",
@@ -2916,6 +2925,43 @@
"repo.settings.pages.trans_cta_headline": "CTA-sectietitel",
"repo.settings.pages.trans_cta_subheadline": "CTA-sectie ondertitel",
"repo.settings.pages.trans_cta_button": "CTA-knoplabel",
"repo.settings.pages.trans_section_brand": "Brand",
"repo.settings.pages.trans_section_hero": "Hero",
"repo.settings.pages.trans_section_stats": "Stats",
"repo.settings.pages.trans_section_value_props": "Value Propositions",
"repo.settings.pages.trans_section_features": "Features",
"repo.settings.pages.trans_section_testimonials": "Testimonials",
"repo.settings.pages.trans_section_pricing": "Pricing",
"repo.settings.pages.trans_section_cta": "Call to Action",
"repo.settings.pages.trans_section_blog": "Blog",
"repo.settings.pages.trans_section_gallery": "Gallery",
"repo.settings.pages.trans_section_comparison": "Comparison",
"repo.settings.pages.trans_section_footer": "Footer",
"repo.settings.pages.trans_section_seo": "SEO",
"repo.settings.pages.trans_brand_name": "Brand Name",
"repo.settings.pages.trans_brand_tagline": "Tagline",
"repo.settings.pages.trans_stat_value": "Value",
"repo.settings.pages.trans_stat_label": "Label",
"repo.settings.pages.trans_title": "Title",
"repo.settings.pages.trans_description": "Description",
"repo.settings.pages.trans_quote": "Quote",
"repo.settings.pages.trans_role": "Role",
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
"repo.settings.pages.trans_plan_name": "Plan Name",
"repo.settings.pages.trans_plan_period": "Period",
"repo.settings.pages.trans_plan_cta": "Plan Button",
"repo.settings.pages.trans_blog_headline": "Blog Headline",
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
"repo.settings.pages.trans_blog_cta": "Blog Button",
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
"repo.settings.pages.trans_footer_copyright": "Copyright",
"repo.settings.pages.trans_footer_link": "Link Label",
"repo.settings.pages.trans_seo_title": "SEO Title",
"repo.settings.pages.trans_seo_description": "SEO Description",
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
@@ -3034,7 +3080,7 @@
"repo.settings.gallery_upload": "Afbeeldingen uploaden",
"repo.settings.gallery_current": "Huidige afbeeldingen",
"repo.settings.gallery_empty": "Nog geen galerij-afbeeldingen. Upload er enkele om uw project te tonen!",
"repo.settings.gallery_uploaded": "Afbeelding succesvol geüpload.",
"repo.settings.gallery_uploaded": "Afbeelding succesvol ge\u00fcpload.",
"repo.settings.gallery_deleted": "Afbeelding succesvol verwijderd.",
"repo.settings.gallery_caption_saved": "Bijschrift succesvol opgeslagen.",
"repo.settings.gallery_caption_placeholder": "Voeg een bijschrift toe...",
@@ -3091,12 +3137,12 @@
"vault.back_to_secret": "Terug naar Geheim",
"vault.back_to_versions": "Terug naar Versies",
"vault.show_value": "Waarde tonen",
"vault.copy_value": "Kopiëren naar klembord",
"vault.copy_value": "Kopi\u00ebren naar klembord",
"vault.view_hidden": "Verborgen bekijken",
"vault.view_raw": "Ruwe waarde bekijken",
"vault.hidden": "Verborgen",
"vault.raw": "Ruw",
"vault.copy": "Kopiëren",
"vault.copy": "Kopi\u00ebren",
"vault.copied": "Gekopieerd!",
"vault.multiline_hint": "Ondersteunt meerregelige inhoud (env-bestanden, certificaten, enz.)",
"vault.rollback": "Terugdraaien",
@@ -3214,5 +3260,18 @@
"repo.view_file": "Bestand bekijken",
"actions.runners.waiting_jobs": "Wachtende taken",
"actions.runners.back_to_runners": "Terug naar runners",
"actions.runners.no_waiting_jobs": "Geen taken wachten op dit label"
"actions.runners.no_waiting_jobs": "Geen taken wachten op dit label",
"repo.settings.pages.cross_promote_section": "Kruispromotie-sectie",
"repo.settings.pages.cross_promote_enabled_desc": "Toon cross-gepromote repositories op de landingspagina",
"repo.settings.pages.cross_promote_headline": "Sectiekop",
"repo.settings.pages.cross_promote_subheadline": "Sectie-ondertitel",
"repo.settings.pages.cross_promote_help": "Configureer de repositories in Instellingen > Kruispromotie.",
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files": "Verborgen bestanden en mappen uitsluiten",
"repo.settings.mirror_settings.push_mirror.exclude_hidden_files_desc": "Bestanden/mappen die beginnen met \".\" of als verborgen gemarkeerd zijn niet naar de remote mirror pushen",
"repo.settings.pages.value_props_headline": "Sectiekop",
"repo.settings.pages.value_props_headline_help": "Grote kop voor de sectie waardeproposities. Het kleine label erboven wordt ingesteld in Sectielabels.",
"repo.settings.pages.value_props_subheadline": "Sectie-ondertitel",
"repo.settings.pages.features_headline": "Sectiekop",
"repo.settings.pages.features_headline_help": "Grote kop voor de sectie functies. Het kleine label erboven wordt ingesteld in Sectielabels.",
"repo.settings.pages.features_subheadline": "Sectie-ondertitel"
}

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -171,10 +171,25 @@ func Routes() *web.Router {
// Upload instructions endpoint
m.Get("/upload/instructions", GetUploadInstructions)
// Public landing page API - for private repos with public_landing enabled
// Landing page API
m.Group("/repos/{owner}/{repo}/pages", func() {
// Public read endpoints
m.Get("/config", repoAssignmentWithPublicAccess(), GetPagesConfig)
m.Get("/content", repoAssignmentWithPublicAccess(), GetPagesContent)
// Write endpoints (require auth + repo admin)
m.Group("/config", func() {
m.Put("", web.Bind(api.UpdatePagesConfigOption{}), UpdatePagesConfig)
m.Patch("", web.Bind(api.UpdatePagesConfigOption{}), PatchPagesConfig)
m.Put("/brand", web.Bind(api.UpdatePagesBrandOption{}), UpdatePagesBrand)
m.Put("/hero", web.Bind(api.UpdatePagesHeroOption{}), UpdatePagesHero)
m.Put("/content", web.Bind(api.UpdatePagesContentOption{}), UpdatePagesContentSection)
m.Put("/comparison", web.Bind(api.UpdatePagesComparisonOption{}), UpdatePagesComparison)
m.Put("/social", web.Bind(api.UpdatePagesSocialOption{}), UpdatePagesSocial)
m.Put("/pricing", web.Bind(api.UpdatePagesPricingOption{}), UpdatePagesPricing)
m.Put("/footer", web.Bind(api.UpdatePagesFooterOption{}), UpdatePagesFooter)
m.Put("/theme", web.Bind(api.UpdatePagesThemeOption{}), UpdatePagesTheme)
}, repoAssignment(), reqToken())
})
// Blog v2 API - repository blog endpoints
@@ -233,6 +248,26 @@ func Routes() *web.Router {
}, reqToken())
}, repoAssignment())
// Organization v2 API - org overview, repos, profile, pinned repos
m.Group("/user/orgs", func() {
m.Get("", ListUserOrgsV2)
}, reqToken())
m.Group("/orgs/{org}", func() {
// Public read endpoints
m.Get("/overview", GetOrgOverviewV2)
m.Get("/repos", ListOrgReposV2)
m.Get("/profile/readme", GetOrgProfileReadmeV2)
// Write endpoints require authentication + org ownership (checked in handler)
m.Group("", func() {
m.Patch("", web.Bind(UpdateOrgV2Option{}), UpdateOrgV2)
m.Put("/profile/readme", web.Bind(UpdateOrgProfileReadmeOption{}), UpdateOrgProfileReadmeV2)
m.Post("/pinned", web.Bind(PinOrgRepoV2Option{}), PinOrgRepoV2)
m.Delete("/pinned/{repo}", UnpinOrgRepoV2)
}, reqToken())
})
// AI settings API - org-scoped AI settings
m.Group("/orgs/{org}/ai", func() {
m.Get("/settings", GetOrgAISettingsV2)

View File

@@ -685,9 +685,11 @@ 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)+len(mcpOrgTools))
allTools = append(allTools, mcpTools...)
allTools = append(allTools, mcpAITools...)
allTools = append(allTools, mcpPagesTools...)
allTools = append(allTools, mcpOrgTools...)
result := MCPToolsListResult{Tools: allTools}
sendMCPResult(ctx, req.ID, result)
}
@@ -759,6 +761,52 @@ 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)
// Organization tools
case "list_orgs":
result, err = toolListOrgs(ctx, params.Arguments)
case "get_org_overview":
result, err = toolGetOrgOverview(ctx, params.Arguments)
case "update_org":
result, err = toolUpdateOrg(ctx, params.Arguments)
case "list_org_repos":
result, err = toolListOrgRepos(ctx, params.Arguments)
case "get_org_profile_readme":
result, err = toolGetOrgProfileReadme(ctx, params.Arguments)
case "update_org_profile_readme":
result, err = toolUpdateOrgProfileReadme(ctx, params.Arguments)
case "pin_org_repo":
result, err = toolPinOrgRepo(ctx, params.Arguments)
case "unpin_org_repo":
result, err = toolUnpinOrgRepo(ctx, params.Arguments)
default:
sendMCPError(ctx, req.ID, -32602, "Unknown tool", params.Name)
return

720
routers/api/v2/mcp_org.go Normal file
View File

@@ -0,0 +1,720 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v2
import (
"errors"
"fmt"
"code.gitcaddy.com/server/v3/models/db"
"code.gitcaddy.com/server/v3/models/organization"
repo_model "code.gitcaddy.com/server/v3/models/repo"
"code.gitcaddy.com/server/v3/modules/optional"
context_service "code.gitcaddy.com/server/v3/services/context"
org_service "code.gitcaddy.com/server/v3/services/org"
user_service "code.gitcaddy.com/server/v3/services/user"
)
// mcpOrgTools defines MCP tools for organization management.
var mcpOrgTools = []MCPTool{
{
Name: "list_orgs",
Description: "List all organizations the authenticated user is a member of. Returns name, full name, description, avatar URL, website, location, group header, and visibility for each org.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{},
},
},
{
Name: "get_org_overview",
Description: "Get a comprehensive overview of an organization including pinned repos, pinned groups, public members, stats (repos/members/teams/stars), profile README content, and recent repository activity.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"org": map[string]any{
"type": "string",
"description": "Organization name",
},
},
"required": []string{"org"},
},
},
{
Name: "update_org",
Description: "Update an organization's basic information. Only provided fields are changed. Requires org owner or site admin.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"org": map[string]any{
"type": "string",
"description": "Organization name",
},
"full_name": map[string]any{
"type": "string",
"description": "Display name of the organization",
},
"email": map[string]any{
"type": "string",
"description": "Contact email address",
},
"description": map[string]any{
"type": "string",
"description": "Organization description",
},
"website": map[string]any{
"type": "string",
"description": "Website URL",
},
"location": map[string]any{
"type": "string",
"description": "Physical location",
},
"group_header": map[string]any{
"type": "string",
"description": "Group header for organizing this org on the explore page",
},
},
"required": []string{"org"},
},
},
{
Name: "list_org_repos",
Description: "List repositories for an organization. Supports grouping by group_header to see how repos are organized. Respects the caller's access permissions.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"org": map[string]any{
"type": "string",
"description": "Organization name",
},
"group_by": map[string]any{
"type": "string",
"description": "Group results by field. Use 'group_header' to see repos grouped by their assigned group.",
"enum": []string{"group_header"},
},
"q": map[string]any{
"type": "string",
"description": "Search keyword to filter repos by name",
},
"sort": map[string]any{
"type": "string",
"description": "Sort order",
"enum": []string{"newest", "oldest", "alphabetically", "reversealphabetically", "stars", "forks", "recentupdate"},
},
"limit": map[string]any{
"type": "number",
"description": "Number of repos to return (max 100, default 50)",
},
"page": map[string]any{
"type": "number",
"description": "Page number (1-based)",
},
},
"required": []string{"org"},
},
},
{
Name: "get_org_profile_readme",
Description: "Get the raw markdown content of an organization's profile README from its .profile repository.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"org": map[string]any{
"type": "string",
"description": "Organization name",
},
},
"required": []string{"org"},
},
},
{
Name: "update_org_profile_readme",
Description: "Update (or create) the organization's profile README. Creates the .profile repository if it doesn't exist. Requires org owner or site admin.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"org": map[string]any{
"type": "string",
"description": "Organization name",
},
"content": map[string]any{
"type": "string",
"description": "Markdown content for the README",
},
"commit_message": map[string]any{
"type": "string",
"description": "Git commit message (default: 'Update organization profile README')",
},
},
"required": []string{"org", "content"},
},
},
{
Name: "pin_org_repo",
Description: "Pin a repository to the organization's overview page. Optionally assign it to a pinned group. Requires org owner or site admin.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"org": map[string]any{
"type": "string",
"description": "Organization name",
},
"repo": map[string]any{
"type": "string",
"description": "Repository name to pin",
},
"group_id": map[string]any{
"type": "number",
"description": "ID of the pinned group to add the repo to (0 or omit for ungrouped)",
},
"display_order": map[string]any{
"type": "number",
"description": "Display order within the group (lower = first)",
},
},
"required": []string{"org", "repo"},
},
},
{
Name: "unpin_org_repo",
Description: "Unpin a repository from the organization's overview page. Requires org owner or site admin.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"org": map[string]any{
"type": "string",
"description": "Organization name",
},
"repo": map[string]any{
"type": "string",
"description": "Repository name to unpin",
},
},
"required": []string{"org", "repo"},
},
},
}
// --- Tool Handlers ---
func toolListOrgs(ctx *context_service.APIContext, _ map[string]any) (any, error) {
if ctx.Doer == nil {
return nil, errors.New("authentication required")
}
opts := organization.FindOrgOptions{
ListOptions: db.ListOptions{PageSize: 50},
UserID: ctx.Doer.ID,
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, ctx.Doer),
}
orgs, _, err := db.FindAndCount[organization.Organization](ctx, opts)
if err != nil {
return nil, err
}
result := make([]map[string]any, 0, len(orgs))
for _, org := range orgs {
entry := map[string]any{
"name": org.Name,
"full_name": org.FullName,
"description": org.Description,
"avatar_url": org.AsUser().AvatarLink(ctx),
"website": org.Website,
"location": org.Location,
"group_header": org.GroupHeader,
"visibility": org.Visibility.String(),
}
result = append(result, entry)
}
return map[string]any{
"organizations": result,
"count": len(result),
}, nil
}
func toolGetOrgOverview(ctx *context_service.APIContext, args map[string]any) (any, error) {
orgName, _ := args["org"].(string)
if orgName == "" {
return nil, errors.New("org is required")
}
org, err := organization.GetOrgByName(ctx, orgName)
if err != nil {
return nil, fmt.Errorf("organization not found: %s", orgName)
}
// Stats
stats, err := org_service.GetOrgOverviewStats(ctx, org.ID, ctx.Doer)
if err != nil {
return nil, err
}
// Pinned repos
pinnedRepos, err := org_service.GetOrgPinnedReposWithDetails(ctx, org.ID)
if err != nil {
return nil, err
}
apiPinned := make([]map[string]any, 0, len(pinnedRepos))
for _, p := range pinnedRepos {
if p.Repo == nil {
continue
}
entry := map[string]any{
"id": p.ID,
"display_order": p.DisplayOrder,
}
if repo, ok := p.Repo.(*repo_model.Repository); ok {
entry["repo_name"] = repo.Name
entry["repo_full_name"] = repo.FullName()
entry["description"] = repo.Description
entry["is_private"] = repo.IsPrivate
}
if p.Group != nil {
entry["group_id"] = p.GroupID
entry["group_name"] = p.Group.Name
}
apiPinned = append(apiPinned, entry)
}
// Pinned groups
pinnedGroups, err := organization.GetOrgPinnedGroups(ctx, org.ID)
if err != nil {
return nil, err
}
apiGroups := make([]map[string]any, 0, len(pinnedGroups))
for _, g := range pinnedGroups {
apiGroups = append(apiGroups, map[string]any{
"id": g.ID,
"name": g.Name,
"display_order": g.DisplayOrder,
"collapsed": g.Collapsed,
})
}
// Profile readme
readme, _ := org_service.GetOrgProfileReadme(ctx, org.ID)
// Recent activity
recentActivity, _ := org_service.GetOrgRecentActivity(ctx, org.ID, ctx.Doer, 10)
apiRecent := make([]map[string]any, 0, len(recentActivity))
for _, a := range recentActivity {
apiRecent = append(apiRecent, map[string]any{
"repo_name": a.RepoName,
"repo_full_name": a.RepoFullName,
"commit_message": a.CommitMessage,
"commit_time": a.CommitTime,
"is_private": a.IsPrivate,
})
}
return map[string]any{
"organization": map[string]any{
"name": org.Name,
"full_name": org.FullName,
"description": org.Description,
"website": org.Website,
"location": org.Location,
"group_header": org.GroupHeader,
"visibility": org.Visibility.String(),
},
"stats": map[string]any{
"total_repos": stats.TotalRepos,
"total_members": stats.TotalMembers,
"total_teams": stats.TotalTeams,
"total_stars": stats.TotalStars,
},
"pinned_repos": apiPinned,
"pinned_groups": apiGroups,
"profile_readme": readme,
"recent_activity": apiRecent,
}, nil
}
func toolUpdateOrg(ctx *context_service.APIContext, args map[string]any) (any, error) {
orgName, _ := args["org"].(string)
if orgName == "" {
return nil, errors.New("org is required")
}
org, err := organization.GetOrgByName(ctx, orgName)
if err != nil {
return nil, fmt.Errorf("organization not found: %s", orgName)
}
// Permission check
if ctx.Doer == nil {
return nil, errors.New("authentication required")
}
if !ctx.Doer.IsAdmin {
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
return nil, err
}
if !isOwner {
return nil, errors.New("organization admin access required")
}
}
opts := &user_service.UpdateOptions{}
updated := []string{}
if v, ok := args["full_name"].(string); ok {
opts.FullName = optional.Some(v)
updated = append(updated, "full_name")
}
if v, ok := args["description"].(string); ok {
opts.Description = optional.Some(v)
updated = append(updated, "description")
}
if v, ok := args["website"].(string); ok {
opts.Website = optional.Some(v)
updated = append(updated, "website")
}
if v, ok := args["location"].(string); ok {
opts.Location = optional.Some(v)
updated = append(updated, "location")
}
if v, ok := args["group_header"].(string); ok {
opts.GroupHeader = optional.Some(v)
updated = append(updated, "group_header")
}
if v, ok := args["email"].(string); ok && v != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), v); err != nil {
return nil, fmt.Errorf("failed to update email: %v", err)
}
updated = append(updated, "email")
}
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
return nil, err
}
return map[string]any{
"status": "updated",
"org": orgName,
"fields_updated": updated,
}, nil
}
func toolListOrgRepos(ctx *context_service.APIContext, args map[string]any) (any, error) {
orgName, _ := args["org"].(string)
if orgName == "" {
return nil, errors.New("org is required")
}
org, err := organization.GetOrgByName(ctx, orgName)
if err != nil {
return nil, fmt.Errorf("organization not found: %s", orgName)
}
limit := 50
if v, ok := args["limit"].(float64); ok && v > 0 {
limit = min(int(v), 100)
}
page := 1
if v, ok := args["page"].(float64); ok && v > 0 {
page = int(v)
}
groupBy, _ := args["group_by"].(string)
keyword, _ := args["q"].(string)
sortOrder, _ := args["sort"].(string)
var orderBy db.SearchOrderBy
switch sortOrder {
case "newest":
orderBy = db.SearchOrderByNewest
case "oldest":
orderBy = db.SearchOrderByOldest
case "reversealphabetically":
orderBy = db.SearchOrderByAlphabeticallyReverse
case "stars":
orderBy = db.SearchOrderByStarsReverse
case "forks":
orderBy = db.SearchOrderByForksReverse
case "recentupdate":
orderBy = db.SearchOrderByRecentUpdated
default:
orderBy = db.SearchOrderByAlphabetically
}
if groupBy == "group_header" {
orderBy = db.SearchOrderBy("CASE WHEN group_header = '' OR group_header IS NULL THEN 1 ELSE 0 END, group_header ASC, " + string(orderBy))
}
repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{PageSize: limit, Page: page},
Keyword: keyword,
OwnerID: org.ID,
OrderBy: orderBy,
Private: ctx.Doer != nil,
Actor: ctx.Doer,
})
if err != nil {
return nil, err
}
if groupBy == "group_header" {
// Grouped response
type repoEntry struct {
Name string `json:"name"`
FullName string `json:"full_name"`
Description string `json:"description"`
IsPrivate bool `json:"is_private"`
IsFork bool `json:"is_fork"`
IsArchived bool `json:"is_archived"`
Stars int `json:"stars"`
Language string `json:"language"`
}
grouped := make(map[string][]repoEntry)
var headers []string
headerSeen := make(map[string]bool)
for _, repo := range repos {
header := repo.GroupHeader
if !headerSeen[header] {
headerSeen[header] = true
headers = append(headers, header)
}
grouped[header] = append(grouped[header], repoEntry{
Name: repo.Name,
FullName: repo.FullName(),
Description: repo.Description,
IsPrivate: repo.IsPrivate,
IsFork: repo.IsFork,
IsArchived: repo.IsArchived,
Stars: repo.NumStars,
Language: repoLanguage(repo),
})
}
groups := make([]map[string]any, 0, len(headers))
for _, h := range headers {
displayName := h
if displayName == "" {
displayName = "(ungrouped)"
}
groups = append(groups, map[string]any{
"group_header": h,
"display_name": displayName,
"repos": grouped[h],
"count": len(grouped[h]),
})
}
return map[string]any{
"groups": groups,
"total": count,
}, nil
}
// Flat response
repoList := make([]map[string]any, 0, len(repos))
for _, repo := range repos {
repoList = append(repoList, map[string]any{
"name": repo.Name,
"full_name": repo.FullName(),
"description": repo.Description,
"is_private": repo.IsPrivate,
"is_fork": repo.IsFork,
"is_archived": repo.IsArchived,
"stars": repo.NumStars,
"language": repoLanguage(repo),
"group_header": repo.GroupHeader,
})
}
return map[string]any{
"repos": repoList,
"total": count,
}, nil
}
func toolGetOrgProfileReadme(ctx *context_service.APIContext, args map[string]any) (any, error) {
orgName, _ := args["org"].(string)
if orgName == "" {
return nil, errors.New("org is required")
}
org, err := organization.GetOrgByName(ctx, orgName)
if err != nil {
return nil, fmt.Errorf("organization not found: %s", orgName)
}
readme, err := org_service.GetOrgProfileReadme(ctx, org.ID)
if err != nil {
return nil, err
}
return map[string]any{
"org": orgName,
"content": readme,
"has_profile": readme != "",
}, nil
}
func toolUpdateOrgProfileReadme(ctx *context_service.APIContext, args map[string]any) (any, error) {
orgName, _ := args["org"].(string)
if orgName == "" {
return nil, errors.New("org is required")
}
content, _ := args["content"].(string)
if content == "" {
return nil, errors.New("content is required")
}
commitMessage, _ := args["commit_message"].(string)
org, err := organization.GetOrgByName(ctx, orgName)
if err != nil {
return nil, fmt.Errorf("organization not found: %s", orgName)
}
// Permission check
if ctx.Doer == nil {
return nil, errors.New("authentication required")
}
if !ctx.Doer.IsAdmin {
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
return nil, err
}
if !isOwner {
return nil, errors.New("organization admin access required")
}
}
if err := org_service.UpdateOrgProfileReadme(ctx, ctx.Doer, org.ID, content, commitMessage); err != nil {
return nil, fmt.Errorf("failed to update profile readme: %v", err)
}
return map[string]any{
"status": "updated",
"org": orgName,
}, nil
}
func toolPinOrgRepo(ctx *context_service.APIContext, args map[string]any) (any, error) {
orgName, _ := args["org"].(string)
repoName, _ := args["repo"].(string)
if orgName == "" || repoName == "" {
return nil, errors.New("org and repo are required")
}
org, err := organization.GetOrgByName(ctx, orgName)
if err != nil {
return nil, fmt.Errorf("organization not found: %s", orgName)
}
// Permission check
if ctx.Doer == nil {
return nil, errors.New("authentication required")
}
if !ctx.Doer.IsAdmin {
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
return nil, err
}
if !isOwner {
return nil, errors.New("organization admin access required")
}
}
repo, err := repo_model.GetRepositoryByName(ctx, org.ID, repoName)
if err != nil {
return nil, fmt.Errorf("repository not found: %s/%s", orgName, repoName)
}
isPinned, err := organization.IsRepoPinned(ctx, org.ID, repo.ID)
if err != nil {
return nil, err
}
if isPinned {
return map[string]any{
"status": "already_pinned",
"org": orgName,
"repo": repoName,
}, nil
}
var groupID int64
if v, ok := args["group_id"].(float64); ok {
groupID = int64(v)
}
var displayOrder int
if v, ok := args["display_order"].(float64); ok {
displayOrder = int(v)
}
pinned := &organization.OrgPinnedRepo{
OrgID: org.ID,
RepoID: repo.ID,
GroupID: groupID,
DisplayOrder: displayOrder,
}
if err := organization.CreateOrgPinnedRepo(ctx, pinned); err != nil {
return nil, err
}
return map[string]any{
"status": "pinned",
"org": orgName,
"repo": repoName,
"id": pinned.ID,
}, nil
}
func toolUnpinOrgRepo(ctx *context_service.APIContext, args map[string]any) (any, error) {
orgName, _ := args["org"].(string)
repoName, _ := args["repo"].(string)
if orgName == "" || repoName == "" {
return nil, errors.New("org and repo are required")
}
org, err := organization.GetOrgByName(ctx, orgName)
if err != nil {
return nil, fmt.Errorf("organization not found: %s", orgName)
}
// Permission check
if ctx.Doer == nil {
return nil, errors.New("authentication required")
}
if !ctx.Doer.IsAdmin {
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
return nil, err
}
if !isOwner {
return nil, errors.New("organization admin access required")
}
}
repo, err := repo_model.GetRepositoryByName(ctx, org.ID, repoName)
if err != nil {
return nil, fmt.Errorf("repository not found: %s/%s", orgName, repoName)
}
if err := organization.DeleteOrgPinnedRepo(ctx, org.ID, repo.ID); err != nil {
return nil, err
}
return map[string]any{
"status": "unpinned",
"org": orgName,
"repo": repoName,
}, nil
}
func repoLanguage(repo *repo_model.Repository) string {
if repo.PrimaryLanguage != nil {
return repo.PrimaryLanguage.Language
}
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

@@ -0,0 +1,526 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v2
import (
"net/http"
"code.gitcaddy.com/server/v3/models/db"
"code.gitcaddy.com/server/v3/models/organization"
"code.gitcaddy.com/server/v3/models/perm"
access_model "code.gitcaddy.com/server/v3/models/perm/access"
repo_model "code.gitcaddy.com/server/v3/models/repo"
apierrors "code.gitcaddy.com/server/v3/modules/errors"
"code.gitcaddy.com/server/v3/modules/optional"
api "code.gitcaddy.com/server/v3/modules/structs"
"code.gitcaddy.com/server/v3/modules/web"
"code.gitcaddy.com/server/v3/services/context"
"code.gitcaddy.com/server/v3/services/convert"
org_service "code.gitcaddy.com/server/v3/services/org"
user_service "code.gitcaddy.com/server/v3/services/user"
)
// --- helpers ---
// loadOrg looks up the org from the path param and checks it exists.
func loadOrg(ctx *context.APIContext) *organization.Organization {
orgName := ctx.PathParam("org")
org, err := organization.GetOrgByName(ctx, orgName)
if err != nil {
if organization.IsErrOrgNotExist(err) {
ctx.APIErrorWithCode(apierrors.OrgNotFound)
} else {
ctx.APIErrorInternal(err)
}
return nil
}
return org
}
// requireOrgOwner checks the doer is an org owner or site admin. Returns false if denied.
func requireOrgOwner(ctx *context.APIContext, org *organization.Organization) bool {
if ctx.Doer.IsAdmin {
return true
}
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return false
}
if !isOwner {
ctx.APIErrorWithCode(apierrors.PermOrgAdminRequired)
return false
}
return true
}
// --- List user's orgs ---
// ListUserOrgsV2 returns all organizations the authenticated user belongs to.
func ListUserOrgsV2(ctx *context.APIContext) {
opts := organization.FindOrgOptions{
ListOptions: db.ListOptions{PageSize: 50},
UserID: ctx.Doer.ID,
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, ctx.Doer),
}
orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiOrgs := make([]*api.Organization, len(orgs))
for i := range orgs {
apiOrgs[i] = convert.ToOrganization(ctx, orgs[i])
}
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, apiOrgs)
}
// --- Org Overview ---
// GetOrgOverviewV2 returns the full organization overview including profile readme and recent activity.
func GetOrgOverviewV2(ctx *context.APIContext) {
org := loadOrg(ctx)
if org == nil {
return
}
// Pinned repos
pinnedRepos, err := org_service.GetOrgPinnedReposWithDetails(ctx, org.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
// Pinned groups
pinnedGroups, err := organization.GetOrgPinnedGroups(ctx, org.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
// Public members
publicMembers, totalMembers, err := organization.GetPublicOrgMembers(ctx, org.ID, 12)
if err != nil {
ctx.APIErrorInternal(err)
return
}
// Stats
stats, err := org_service.GetOrgOverviewStats(ctx, org.ID, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
// Profile readme
readme, _ := org_service.GetOrgProfileReadme(ctx, org.ID)
// Recent activity
recentActivity, _ := org_service.GetOrgRecentActivity(ctx, org.ID, ctx.Doer, 10)
// Convert pinned repos
apiPinnedRepos := make([]*api.OrgPinnedRepo, 0, len(pinnedRepos))
for _, p := range pinnedRepos {
if p.Repo == nil {
continue
}
apiPinnedRepos = append(apiPinnedRepos, convertOrgPinnedRepoV2(ctx, p))
}
apiPinnedGroups := make([]*api.OrgPinnedGroup, 0, len(pinnedGroups))
for _, g := range pinnedGroups {
apiPinnedGroups = append(apiPinnedGroups, convertOrgPinnedGroupV2(g))
}
apiPublicMembers := make([]*api.OrgPublicMember, 0, len(publicMembers))
for _, m := range publicMembers {
apiPublicMembers = append(apiPublicMembers, &api.OrgPublicMember{
User: convert.ToUser(ctx, m.User, ctx.Doer),
Role: m.Role,
})
}
// Build profile content
var profile *api.OrgProfileContent
if readme != "" {
profile = &api.OrgProfileContent{
HasProfile: true,
Readme: readme,
}
}
// Build recent activity
var apiRecentActivity []api.OrgRecentActivity
if len(recentActivity) > 0 {
apiRecentActivity = make([]api.OrgRecentActivity, 0, len(recentActivity))
for _, a := range recentActivity {
apiRecentActivity = append(apiRecentActivity, api.OrgRecentActivity{
RepoName: a.RepoName,
RepoFullName: a.RepoFullName,
DefaultBranch: a.DefaultBranch,
CommitMessage: a.CommitMessage,
CommitTime: a.CommitTime,
IsPrivate: a.IsPrivate,
})
}
}
overview := &api.OrgOverviewV2{
Organization: convert.ToOrganization(ctx, org),
PinnedRepos: apiPinnedRepos,
PinnedGroups: apiPinnedGroups,
PublicMembers: apiPublicMembers,
TotalMembers: totalMembers,
Stats: &api.OrgOverviewStats{
TotalRepos: stats.TotalRepos,
TotalMembers: stats.TotalMembers,
TotalTeams: stats.TotalTeams,
TotalStars: stats.TotalStars,
},
Profile: profile,
RecentActivity: apiRecentActivity,
}
ctx.JSON(http.StatusOK, overview)
}
// --- Update Org ---
// UpdateOrgV2Option represents the fields that can be updated on an org.
type UpdateOrgV2Option struct {
FullName *string `json:"full_name"`
Email *string `json:"email"`
Description *string `json:"description"`
Website *string `json:"website"`
Location *string `json:"location"`
GroupHeader *string `json:"group_header"`
}
// UpdateOrgV2 updates an organization's basic information.
func UpdateOrgV2(ctx *context.APIContext) {
org := loadOrg(ctx)
if org == nil {
return
}
if !requireOrgOwner(ctx, org) {
return
}
form := web.GetForm(ctx).(*UpdateOrgV2Option)
opts := &user_service.UpdateOptions{}
if form.FullName != nil {
opts.FullName = optional.Some(*form.FullName)
}
if form.Description != nil {
opts.Description = optional.Some(*form.Description)
}
if form.Website != nil {
opts.Website = optional.Some(*form.Website)
}
if form.Location != nil {
opts.Location = optional.Some(*form.Location)
}
if form.GroupHeader != nil {
opts.GroupHeader = optional.Some(*form.GroupHeader)
}
if form.Email != nil && *form.Email != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), *form.Email); err != nil {
ctx.APIErrorInternal(err)
return
}
}
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
ctx.APIErrorInternal(err)
return
}
// Reload to get updated fields
org, err := organization.GetOrgByName(ctx, org.Name)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, org))
}
// --- List Org Repos ---
// ListOrgReposV2 lists all repos for an org, with optional group_header grouping.
func ListOrgReposV2(ctx *context.APIContext) {
org := loadOrg(ctx)
if org == nil {
return
}
page := ctx.FormInt("page")
if page <= 0 {
page = 1
}
limit := ctx.FormInt("limit")
if limit <= 0 || limit > 100 {
limit = 50
}
groupBy := ctx.FormString("group_by")
keyword := ctx.FormTrim("q")
sortOrder := ctx.FormString("sort")
var orderBy db.SearchOrderBy
switch sortOrder {
case "newest":
orderBy = db.SearchOrderByNewest
case "oldest":
orderBy = db.SearchOrderByOldest
case "reversealphabetically":
orderBy = db.SearchOrderByAlphabeticallyReverse
case "stars":
orderBy = db.SearchOrderByStarsReverse
case "forks":
orderBy = db.SearchOrderByForksReverse
case "recentupdate":
orderBy = db.SearchOrderByRecentUpdated
default:
orderBy = db.SearchOrderByAlphabetically
}
// When grouping by group_header, prepend it to the sort
if groupBy == "group_header" {
orderBy = db.SearchOrderBy("CASE WHEN group_header = '' OR group_header IS NULL THEN 1 ELSE 0 END, group_header ASC, " + string(orderBy))
}
repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{PageSize: limit, Page: page},
Keyword: keyword,
OwnerID: org.ID,
OrderBy: orderBy,
Private: ctx.IsSigned,
Actor: ctx.Doer,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiRepos := make([]*api.Repository, 0, len(repos))
for _, repo := range repos {
apiRepos = append(apiRepos, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeRead}))
}
if groupBy == "group_header" {
// Return grouped response
type groupedEntry struct {
GroupHeader string `json:"group_header"`
Repos []*api.Repository `json:"repos"`
}
grouped := make(map[string][]*api.Repository)
var headers []string
headerSeen := make(map[string]bool)
for i, repo := range repos {
header := repo.GroupHeader
if !headerSeen[header] {
headerSeen[header] = true
headers = append(headers, header)
}
grouped[header] = append(grouped[header], apiRepos[i])
}
groups := make([]groupedEntry, 0, len(headers))
for _, h := range headers {
groups = append(groups, groupedEntry{
GroupHeader: h,
Repos: grouped[h],
})
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, map[string]any{
"groups": groups,
"total": count,
})
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiRepos)
}
// --- Profile Readme ---
// GetOrgProfileReadmeV2 returns the raw README content from the .profile repo.
func GetOrgProfileReadmeV2(ctx *context.APIContext) {
org := loadOrg(ctx)
if org == nil {
return
}
readme, err := org_service.GetOrgProfileReadme(ctx, org.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, map[string]any{
"content": readme,
"has_profile": readme != "",
})
}
// UpdateOrgProfileReadmeOption represents a request to update the profile readme.
type UpdateOrgProfileReadmeOption struct {
Content string `json:"content" binding:"Required"`
CommitMessage string `json:"commit_message"`
}
// UpdateOrgProfileReadmeV2 updates the README.md in the .profile repo.
func UpdateOrgProfileReadmeV2(ctx *context.APIContext) {
org := loadOrg(ctx)
if org == nil {
return
}
if !requireOrgOwner(ctx, org) {
return
}
form := web.GetForm(ctx).(*UpdateOrgProfileReadmeOption)
if err := org_service.UpdateOrgProfileReadme(ctx, ctx.Doer, org.ID, form.Content, form.CommitMessage); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, map[string]any{
"status": "ok",
})
}
// --- Pinned Repos ---
// PinOrgRepoV2Option represents a request to pin a repo.
type PinOrgRepoV2Option struct {
RepoName string `json:"repo_name" binding:"Required"`
GroupID int64 `json:"group_id"`
DisplayOrder int `json:"display_order"`
}
// PinOrgRepoV2 pins a repository to the org overview.
func PinOrgRepoV2(ctx *context.APIContext) {
org := loadOrg(ctx)
if org == nil {
return
}
if !requireOrgOwner(ctx, org) {
return
}
form := web.GetForm(ctx).(*PinOrgRepoV2Option)
// Look up the repo
repo, err := repo_model.GetRepositoryByName(ctx, org.ID, form.RepoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIError(http.StatusNotFound, "Repository not found: "+form.RepoName)
} else {
ctx.APIErrorInternal(err)
}
return
}
// Check if already pinned
isPinned, err := organization.IsRepoPinned(ctx, org.ID, repo.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if isPinned {
ctx.JSON(http.StatusOK, map[string]any{"status": "already_pinned"})
return
}
pinned := &organization.OrgPinnedRepo{
OrgID: org.ID,
RepoID: repo.ID,
GroupID: form.GroupID,
DisplayOrder: form.DisplayOrder,
}
if err := organization.CreateOrgPinnedRepo(ctx, pinned); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, map[string]any{
"id": pinned.ID,
"repo_id": repo.ID,
"status": "pinned",
})
}
// UnpinOrgRepoV2 unpins a repository from the org overview.
func UnpinOrgRepoV2(ctx *context.APIContext) {
org := loadOrg(ctx)
if org == nil {
return
}
if !requireOrgOwner(ctx, org) {
return
}
repoName := ctx.PathParam("repo")
repo, err := repo_model.GetRepositoryByName(ctx, org.ID, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIError(http.StatusNotFound, "Repository not found: "+repoName)
} else {
ctx.APIErrorInternal(err)
}
return
}
if err := organization.DeleteOrgPinnedRepo(ctx, org.ID, repo.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// --- Converters ---
func convertOrgPinnedRepoV2(ctx *context.APIContext, p *organization.OrgPinnedRepo) *api.OrgPinnedRepo {
result := &api.OrgPinnedRepo{
ID: p.ID,
RepoID: p.RepoID,
GroupID: p.GroupID,
DisplayOrder: p.DisplayOrder,
}
if p.Repo != nil {
if repo, ok := p.Repo.(*repo_model.Repository); ok {
result.Repo = convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeRead})
}
}
if p.Group != nil {
result.Group = convertOrgPinnedGroupV2(p.Group)
}
return result
}
func convertOrgPinnedGroupV2(g *organization.OrgPinnedGroup) *api.OrgPinnedGroup {
return &api.OrgPinnedGroup{
ID: g.ID,
Name: g.Name,
DisplayOrder: g.DisplayOrder,
Collapsed: g.Collapsed,
}
}

View File

@@ -7,22 +7,39 @@ import (
"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"
"code.gitcaddy.com/server/v3/modules/json"
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"
)
// PagesConfigResponse represents the pages configuration for a repository
type PagesConfigResponse 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"`
SEO pages_module.SEOConfig `json:"seo"`
Footer pages_module.FooterConfig `json:"footer"`
// 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
@@ -32,7 +49,433 @@ type PagesContentResponse struct {
Readme string `json:"readme,omitempty"`
}
// GetPagesConfig returns the pages configuration for a repository
// ---------------------------------------------------------------------------
// 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.StaticRoutes != nil {
dst.StaticRoutes = *src.StaticRoutes
}
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
@@ -43,22 +486,11 @@ func GetPagesConfig(ctx *context.APIContext) {
config, err := pages_service.GetPagesConfig(ctx, repo)
if err != nil {
ctx.APIErrorNotFound("Pages not configured")
ctx.APIErrorWithCode(apierrors.PagesNotConfigured)
return
}
response := &PagesConfigResponse{
Enabled: config.Enabled,
PublicLanding: config.PublicLanding,
Template: config.Template,
Domain: config.Domain,
Brand: config.Brand,
Hero: config.Hero,
SEO: config.SEO,
Footer: config.Footer,
}
ctx.JSON(http.StatusOK, response)
ctx.JSON(http.StatusOK, buildFullResponse(config))
}
// GetPagesContent returns the rendered content for a repository's landing page
@@ -72,14 +504,12 @@ func GetPagesContent(ctx *context.APIContext) {
config, err := pages_service.GetPagesConfig(ctx, repo)
if err != nil || !config.Enabled {
ctx.APIErrorNotFound("Pages not enabled")
ctx.APIErrorWithCode(apierrors.PagesNotEnabled)
return
}
// Load README content
readme := loadReadmeContent(ctx, repo)
// Build title
title := config.SEO.Title
if title == "" {
title = config.Hero.Headline
@@ -91,7 +521,6 @@ func GetPagesContent(ctx *context.APIContext) {
title = repo.Name
}
// Build description
description := config.SEO.Description
if description == "" {
description = config.Hero.Subheadline
@@ -100,15 +529,265 @@ func GetPagesContent(ctx *context.APIContext) {
description = repo.Description
}
response := &PagesContentResponse{
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
}
ctx.JSON(http.StatusOK, response)
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())
@@ -127,7 +806,6 @@ func loadReadmeContent(ctx *context.APIContext, repo *repo_model.Repository) str
return ""
}
// Try common README paths
readmePaths := []string{
"README.md",
"readme.md",

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

@@ -93,7 +93,12 @@ func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, t
}
opts.Keyword = ctx.FormTrim("q")
opts.OrderBy = orderBy
// When grouping is enabled, order by group first so groups aren't split across pages
if ctx.Data["PageIsExploreOrganizations"] == true && ctx.Data["ShowGrouping"] == true {
opts.OrderBy = "CASE WHEN `user`.group_header = '' OR `user`.group_header IS NULL THEN 1 ELSE 0 END, `user`.group_header ASC, " + orderBy
} else {
opts.OrderBy = orderBy
}
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
users, count, err = user_model.SearchUsers(ctx, opts)
if err != nil {

View File

@@ -14,6 +14,7 @@ import (
_ "image/jpeg" // register JPEG decoder for social card background images
_ "image/png" // register PNG decoder for social card background images
"io"
"maps"
"math/big"
"net/http"
"path"
@@ -75,6 +76,15 @@ func ServeLandingPage(ctx *context.Context) {
}
}
// Check for static route file serving
if len(config.Advanced.StaticRoutes) > 0 {
cleanPath := path.Clean(requestPath)
if matchesStaticRoute(cleanPath, config.Advanced.StaticRoutes) {
serveStaticRouteFile(ctx, repo, cleanPath)
return
}
}
// Handle event tracking POST
if ctx.Req.Method == http.MethodPost && (requestPath == "/pages/events" || requestPath == "/pages/events/") {
servePageEvent(ctx, repo)
@@ -264,6 +274,85 @@ func renderLandingPage(ctx *context.Context, repo *repo_model.Repository, config
}
}
// Load cross-promoted repos if enabled, filtered to those with landing pages
if config.CrossPromote.Enabled {
records, err := repo_model.GetCrossPromotedRepos(ctx, repo.ID)
if err != nil {
log.Warn("GetCrossPromotedRepos for repo %d: %v", repo.ID, err)
} else if len(records) > 0 {
repoIDs := make([]int64, len(records))
for i, r := range records {
repoIDs[i] = r.TargetRepoID
}
repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs)
if err != nil {
log.Warn("GetRepositoriesMapByIDs for cross-promote: %v", err)
} else {
type CrossPromoteItem struct {
Name string
Description string
URL string
LogoURL string
}
var items []CrossPromoteItem
for _, r := range records {
target, ok := repos[r.TargetRepoID]
if !ok {
log.Trace("Cross-promote target repo %d not found", r.TargetRepoID)
continue
}
// Only require that the target repo has landing pages enabled —
// the landing page is publicly reachable independent of the repo's
// private flag, so we don't filter by target.IsPrivate.
pagesEnabled, err := repo_model.IsPagesEnabled(ctx, target.ID)
if err != nil || !pagesEnabled {
log.Trace("Cross-promote target %s does not have pages enabled (err=%v, enabled=%v)", target.FullName(), err, pagesEnabled)
continue
}
item := CrossPromoteItem{
Description: target.Description,
Name: target.Name,
}
// URL priority:
// 1. Repo's explicit Website field (dev-controlled)
// 2. Verified custom domain for pages
// 3. Internal /pages URL
switch {
case target.Website != "":
item.URL = target.Website
default:
item.URL = target.Link() + "/pages"
if domains, err := repo_model.GetPagesDomainsByRepoID(ctx, target.ID); err == nil {
for _, d := range domains {
if d.Verified {
item.URL = "https://" + d.Domain
break
}
}
}
}
// Try to load brand info from pages config (optional)
if targetConfig, err := pages_service.GetPagesConfig(ctx, target); err == nil && targetConfig != nil {
if targetConfig.Brand.Name != "" {
item.Name = targetConfig.Brand.Name
}
if targetConfig.Brand.LogoURL != "" {
item.LogoURL = targetConfig.Brand.LogoURL
} else if targetConfig.Brand.UploadedLogo != "" {
item.LogoURL = "/repo-avatars/" + targetConfig.Brand.UploadedLogo
}
}
items = append(items, item)
}
if len(items) > 0 {
ctx.Data["CrossPromoteItems"] = items
} else {
log.Trace("Cross-promote enabled for repo %d but no eligible target repos", repo.ID)
}
}
}
}
tpl := selectTemplate(config.Template)
ctx.HTML(http.StatusOK, tpl)
}
@@ -592,6 +681,89 @@ func serveCustomDomainAsset(ctx *context.Context, repo *repo_model.Repository, a
serveRepoFileAsset(ctx, commit, assetPath)
}
// matchesStaticRoute checks if a request path matches any configured static route pattern.
// Supports exact matches (/badge.svg) and glob patterns (/schema/*).
func matchesStaticRoute(requestPath string, routes []string) bool {
for _, route := range routes {
if route == requestPath {
return true
}
// Handle glob patterns like /schema/*
if matched, _ := path.Match(route, requestPath); matched {
return true
}
// Handle prefix patterns: /schema/* should match /schema/sub/deep.json
if strings.HasSuffix(route, "/*") {
prefix := strings.TrimSuffix(route, "*")
if strings.HasPrefix(requestPath, prefix) {
return true
}
}
}
return false
}
// serveStaticRouteFile serves a file directly from the repo tree for static route matches.
// The request path maps directly to the repo root (e.g., /schema/v0.1.json → schema/v0.1.json).
func serveStaticRouteFile(ctx *context.Context, repo *repo_model.Repository, requestPath string) {
// Strip leading slash to get repo-relative path
repoPath := strings.TrimPrefix(requestPath, "/")
if repoPath == "" {
ctx.NotFound(errors.New("empty static route path"))
return
}
// Security: never serve .gitea config files
if strings.HasPrefix(repoPath, ".gitea/") || strings.HasPrefix(repoPath, ".gitea\\") {
ctx.NotFound(errors.New("config files are not served"))
return
}
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
if err != nil {
ctx.NotFound(err)
return
}
defer gitRepo.Close()
branch := repo.DefaultBranch
if branch == "" {
branch = "main"
}
commit, err := gitRepo.GetBranchCommit(branch)
if err != nil {
ctx.NotFound(err)
return
}
entry, err := commit.GetTreeEntryByPath(repoPath)
if err != nil {
ctx.NotFound(err)
return
}
// Only serve blobs, not trees
if !entry.IsRegular() {
ctx.NotFound(errors.New("path is not a file"))
return
}
reader, err := entry.Blob().DataAsync()
if err != nil {
ctx.ServerError("Failed to read static route file", err)
return
}
defer reader.Close()
ext := path.Ext(repoPath)
contentType := getContentType(ext)
ctx.Resp.Header().Set("Content-Type", contentType)
ctx.Resp.Header().Set("Cache-Control", "public, max-age=3600")
ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(entry.Blob().Size(), 10))
_, _ = io.Copy(ctx.Resp, reader)
}
// serveSocialPreview generates and serves the social card image for a repo
// on custom domain / subdomain requests.
func serveSocialPreview(ctx *context.Context, repo *repo_model.Repository) {
@@ -1070,6 +1242,8 @@ func deepMergeConfig(base *pages_module.LandingConfig, overrideJSON string) (*pa
}
// deepMerge recursively merges src into dst.
// Maps are merged recursively; arrays are merged element-wise (preserving
// base fields like icons that the translation overlay may omit).
func deepMerge(dst, src map[string]any) map[string]any {
for key, srcVal := range src {
if dstVal, ok := dst[key]; ok {
@@ -1080,12 +1254,45 @@ func deepMerge(dst, src map[string]any) map[string]any {
dst[key] = deepMerge(dstMap, srcMap)
continue
}
// Both are arrays: merge element-wise
srcArr, srcIsArr := srcVal.([]any)
dstArr, dstIsArr := dstVal.([]any)
if srcIsArr && dstIsArr {
dst[key] = deepMergeArrays(dstArr, srcArr)
continue
}
}
dst[key] = srcVal
}
return dst
}
// deepMergeArrays merges two arrays element-wise. For each index, if both
// elements are maps, they are deep-merged (so translation fields override
// base fields while preserving untranslated fields like icons). Otherwise
// the source element replaces the base.
func deepMergeArrays(dst, src []any) []any {
result := make([]any, max(len(dst), len(src)))
for i := range result {
if i < len(src) && i < len(dst) {
srcMap, srcIsMap := src[i].(map[string]any)
dstMap, dstIsMap := dst[i].(map[string]any)
if srcIsMap && dstIsMap {
merged := make(map[string]any, len(dstMap))
maps.Copy(merged, dstMap)
result[i] = deepMerge(merged, srcMap)
} else {
result[i] = src[i]
}
} else if i < len(src) {
result[i] = src[i]
} else {
result[i] = dst[i]
}
}
return result
}
// detectPageLanguage determines the active language for a landing page.
// Priority: ?lang= query param > pages_lang cookie > Accept-Language header > default.
func detectPageLanguage(ctx *context.Context, config *pages_module.LandingConfig) string {
@@ -1138,6 +1345,11 @@ func detectPageLanguage(ctx *context.Context, config *pages_module.LandingConfig
// applyLanguageOverlay loads the translation for the detected language and merges it onto config.
// Sets template data for the language switcher and returns the (possibly merged) config.
func applyLanguageOverlay(ctx *context.Context, repo *repo_model.Repository, config *pages_module.LandingConfig) *pages_module.LandingConfig {
// Ensure navigation labels and section headlines are populated with
// template-specific defaults so they are present in the base config
// for deep-merge (and so templates never fall back to hardcoded English).
ensureTemplateDefaults(config)
if len(config.I18n.Languages) == 0 {
return config
}
@@ -1179,6 +1391,50 @@ func applyLanguageOverlay(ctx *context.Context, repo *repo_model.Repository, con
return merged
}
// ensureTemplateDefaults fills in empty navigation labels and section headlines
// with template-specific defaults so they are always present for serialization
// and deep-merge. This ensures the base config JSON contains these keys, making
// them available for the translation overlay to override.
func ensureTemplateDefaults(config *pages_module.LandingConfig) {
defaults := pages_module.TemplateDefaultLabels(config.Template)
nav := &config.Navigation
if nav.LabelValueProps == "" {
nav.LabelValueProps = defaults.LabelValueProps
}
if nav.LabelFeatures == "" {
nav.LabelFeatures = defaults.LabelFeatures
}
if nav.LabelPricing == "" {
nav.LabelPricing = defaults.LabelPricing
}
if nav.LabelBlog == "" {
nav.LabelBlog = defaults.LabelBlog
}
if nav.LabelGallery == "" {
nav.LabelGallery = defaults.LabelGallery
}
if nav.LabelCompare == "" {
nav.LabelCompare = defaults.LabelCompare
}
if nav.LabelCrossPromote == "" {
nav.LabelCrossPromote = defaults.LabelCrossPromote
}
// Section headlines — fill empty headlines with sensible defaults
// so they appear in the base JSON and can be overridden by translations.
if config.Blog.Enabled && config.Blog.Headline == "" {
config.Blog.Headline = "Latest Posts"
}
if config.Gallery.Enabled && config.Gallery.Headline == "" {
config.Gallery.Headline = "Gallery"
}
if config.Comparison.Enabled && config.Comparison.Headline == "" {
config.Comparison.Headline = "How We Compare"
}
if config.CrossPromote.Enabled && config.CrossPromote.Headline == "" {
config.CrossPromote.Headline = "Related Offerings"
}
}
// ApproveExperiment handles the email approval link for an A/B test experiment
func ApproveExperiment(ctx *context.Context) {
handleExperimentAction(ctx, true)

View File

@@ -10,6 +10,7 @@ import (
"net/http"
"slices"
"strings"
"sync"
pages_model "code.gitcaddy.com/server/v3/models/pages"
repo_model "code.gitcaddy.com/server/v3/models/repo"
@@ -36,6 +37,7 @@ const (
tplRepoSettingsPagesFooter templates.TplName = "repo/settings/pages_footer"
tplRepoSettingsPagesTheme templates.TplName = "repo/settings/pages_theme"
tplRepoSettingsPagesLanguages templates.TplName = "repo/settings/pages_languages"
tplRepoSettingsPagesAdvanced templates.TplName = "repo/settings/pages_advanced"
)
// getPagesLandingConfig loads the landing page configuration
@@ -86,6 +88,34 @@ func savePagesLandingConfig(ctx *context.Context, config *pages_module.LandingCo
return repo_model.UpdatePagesConfig(ctx, dbConfig)
}
// applyTemplateDefaultLabels populates Navigation label fields with
// template-specific defaults. Existing non-empty labels are preserved.
func applyTemplateDefaultLabels(config *pages_module.LandingConfig) {
defaults := pages_module.TemplateDefaultLabels(config.Template)
nav := &config.Navigation
if nav.LabelValueProps == "" {
nav.LabelValueProps = defaults.LabelValueProps
}
if nav.LabelFeatures == "" {
nav.LabelFeatures = defaults.LabelFeatures
}
if nav.LabelPricing == "" {
nav.LabelPricing = defaults.LabelPricing
}
if nav.LabelBlog == "" {
nav.LabelBlog = defaults.LabelBlog
}
if nav.LabelGallery == "" {
nav.LabelGallery = defaults.LabelGallery
}
if nav.LabelCompare == "" {
nav.LabelCompare = defaults.LabelCompare
}
if nav.LabelCrossPromote == "" {
nav.LabelCrossPromote = defaults.LabelCrossPromote
}
}
// setCommonPagesData sets common data for all pages settings pages
func setCommonPagesData(ctx *context.Context) {
config := getPagesLandingConfig(ctx)
@@ -140,6 +170,7 @@ func PagesPost(ctx *context.Context) {
config := getPagesLandingConfig(ctx)
config.Enabled = true
config.Template = template
applyTemplateDefaultLabels(config)
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("EnablePages", err)
return
@@ -160,6 +191,7 @@ func PagesPost(ctx *context.Context) {
}
config := getPagesLandingConfig(ctx)
config.Template = template
applyTemplateDefaultLabels(config)
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("UpdateTemplate", err)
return
@@ -450,6 +482,12 @@ func PagesContentPost(ctx *context.Context) {
config.Advanced.HideMobileReleases = ctx.FormBool("hide_mobile_releases")
config.Advanced.GooglePlayID = strings.TrimSpace(ctx.FormString("google_play_id"))
config.Advanced.AppStoreID = strings.TrimSpace(ctx.FormString("app_store_id"))
if v := ctx.FormString("label_value_props"); v != "" {
config.Navigation.LabelValueProps = v
}
if v := ctx.FormString("label_features"); v != "" {
config.Navigation.LabelFeatures = v
}
config.Navigation.ShowDocs = ctx.FormBool("nav_show_docs")
config.Navigation.ShowAPI = ctx.FormBool("nav_show_api")
config.Navigation.ShowRepository = ctx.FormBool("nav_show_repository")
@@ -479,6 +517,8 @@ func PagesContentPost(ctx *context.Context) {
}
config.Stats = append(config.Stats, pages_module.StatConfig{Value: value, Label: label})
}
config.ValuePropsHeadline = ctx.FormString("value_props_headline")
config.ValuePropsSubheadline = ctx.FormString("value_props_subheadline")
config.ValueProps = nil
for i := range 10 {
title := ctx.FormString(fmt.Sprintf("valueprop_title_%d", i))
@@ -489,6 +529,8 @@ func PagesContentPost(ctx *context.Context) {
}
config.ValueProps = append(config.ValueProps, pages_module.ValuePropConfig{Title: title, Description: desc, Icon: icon})
}
config.FeaturesHeadline = ctx.FormString("features_headline")
config.FeaturesSubheadline = ctx.FormString("features_subheadline")
config.Features = nil
for i := range 20 {
title := ctx.FormString(fmt.Sprintf("feature_title_%d", i))
@@ -502,6 +544,12 @@ func PagesContentPost(ctx *context.Context) {
}
// Comparison enabled toggle (full config is on /settings/pages/comparison)
config.Comparison.Enabled = ctx.FormBool("comparison_enabled")
// Cross-promote section
config.CrossPromote.Enabled = ctx.FormBool("cross_promote_enabled")
if v := ctx.FormString("cross_promote_headline"); v != "" {
config.CrossPromote.Headline = v
}
config.CrossPromote.Subheadline = ctx.FormString("cross_promote_subheadline")
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
@@ -716,19 +764,110 @@ func PagesThemePost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/theme")
}
// TranslationView is a flattened view of a translation for the settings UI
// TranslationView is a flattened view of a translation for the settings UI.
// Slice fields are padded to match the base config array lengths.
type TranslationView struct {
Headline string
Subheadline string
PrimaryCTA string
SecondaryCTA string
// Brand
BrandName string
BrandTagline string
// Hero
Headline string
Subheadline string
PrimaryCTA string
SecondaryCTA string
// Stats (parallel to Config.Stats)
StatsValues []string
StatsLabels []string
// Value Props (parallel to Config.ValueProps)
ValuePropTitles []string
ValuePropDescs []string
// Features (parallel to Config.Features)
FeatureTitles []string
FeatureDescs []string
// Testimonials (parallel to Config.SocialProof.Testimonials)
TestimonialQuotes []string
TestimonialRoles []string
// Pricing
PricingHeadline string
PricingSubheadline string
PlanNames []string
PlanPeriods []string
PlanCTAs []string
// CTA Section
CTAHeadline string
CTASubheadline string
CTAButton string
// Value Props section
ValuePropsHeadline string
ValuePropsSubheadline string
// Features section
FeaturesHeadline string
FeaturesSubheadline string
// Blog
BlogHeadline string
BlogSubheadline string
BlogCTAButton string
// Gallery
GalleryHeadline string
GallerySubheadline string
// Comparison
ComparisonHeadline string
ComparisonSubheadline string
// Cross-Promote
CrossPromoteHeadline string
CrossPromoteSubheadline string
// Footer
FooterCopyright string
FooterLinkLabels []string
// SEO
SEOTitle string
SEODescription string
// Navigation labels
NavLabelValueProps string
NavLabelFeatures string
NavLabelPricing string
NavLabelBlog string
NavLabelGallery string
NavLabelCompare string
NavLabelCrossPromote string
NavLabelDocs string
NavLabelReleases string
NavLabelAPI string
NavLabelIssues string
}
// parseTranslationView parses a PagesTranslation into a flat view for the template
func parseTranslationView(t *pages_model.Translation) *TranslationView {
// overlayString extracts a string from a map
func overlayString(m map[string]any, key string) string {
if v, ok := m[key].(string); ok {
return v
}
return ""
}
// overlayStringSlice extracts a []string-field slice from an overlay array of objects.
// Each array element is a map; fieldKey is the key to extract from each map.
func overlayStringSlice(overlay map[string]any, arrayKey, fieldKey string, padLen int) []string {
result := make([]string, padLen)
arr, ok := overlay[arrayKey].([]any)
if !ok {
return result
}
for i, item := range arr {
if i >= padLen {
break
}
if m, ok := item.(map[string]any); ok {
if v, ok := m[fieldKey].(string); ok {
result[i] = v
}
}
}
return result
}
// parseTranslationView parses a PagesTranslation into a flat view for the template.
// config is used to determine array lengths for padding slices.
func parseTranslationView(t *pages_model.Translation, config *pages_module.LandingConfig) *TranslationView {
if t == nil || t.ConfigJSON == "" {
return nil
}
@@ -738,37 +877,160 @@ func parseTranslationView(t *pages_model.Translation) *TranslationView {
}
view := &TranslationView{}
// Brand
if brand, ok := overlay["brand"].(map[string]any); ok {
view.BrandName = overlayString(brand, "name")
view.BrandTagline = overlayString(brand, "tagline")
}
// Hero
if hero, ok := overlay["hero"].(map[string]any); ok {
if v, ok := hero["headline"].(string); ok {
view.Headline = v
}
if v, ok := hero["subheadline"].(string); ok {
view.Subheadline = v
}
view.Headline = overlayString(hero, "headline")
view.Subheadline = overlayString(hero, "subheadline")
if cta, ok := hero["primary_cta"].(map[string]any); ok {
if v, ok := cta["label"].(string); ok {
view.PrimaryCTA = v
}
view.PrimaryCTA = overlayString(cta, "label")
}
if cta, ok := hero["secondary_cta"].(map[string]any); ok {
if v, ok := cta["label"].(string); ok {
view.SecondaryCTA = v
view.SecondaryCTA = overlayString(cta, "label")
}
}
// Stats
view.StatsValues = overlayStringSlice(overlay, "stats", "value", len(config.Stats))
view.StatsLabels = overlayStringSlice(overlay, "stats", "label", len(config.Stats))
// Value Props
view.ValuePropTitles = overlayStringSlice(overlay, "value_props", "title", len(config.ValueProps))
view.ValuePropDescs = overlayStringSlice(overlay, "value_props", "description", len(config.ValueProps))
view.ValuePropsHeadline = overlayString(overlay, "value_props_headline")
view.ValuePropsSubheadline = overlayString(overlay, "value_props_subheadline")
// Features
view.FeatureTitles = overlayStringSlice(overlay, "features", "title", len(config.Features))
view.FeatureDescs = overlayStringSlice(overlay, "features", "description", len(config.Features))
view.FeaturesHeadline = overlayString(overlay, "features_headline")
view.FeaturesSubheadline = overlayString(overlay, "features_subheadline")
// Testimonials (stored under social_proof.testimonials)
view.TestimonialQuotes = make([]string, len(config.SocialProof.Testimonials))
view.TestimonialRoles = make([]string, len(config.SocialProof.Testimonials))
if sp, ok := overlay["social_proof"].(map[string]any); ok {
if arr, ok := sp["testimonials"].([]any); ok {
for i, item := range arr {
if i >= len(config.SocialProof.Testimonials) {
break
}
if m, ok := item.(map[string]any); ok {
view.TestimonialQuotes[i] = overlayString(m, "quote")
view.TestimonialRoles[i] = overlayString(m, "role")
}
}
}
}
// Pricing
if pricing, ok := overlay["pricing"].(map[string]any); ok {
view.PricingHeadline = overlayString(pricing, "headline")
view.PricingSubheadline = overlayString(pricing, "subheadline")
if plans, ok := pricing["plans"].([]any); ok {
view.PlanNames = make([]string, len(config.Pricing.Plans))
view.PlanPeriods = make([]string, len(config.Pricing.Plans))
view.PlanCTAs = make([]string, len(config.Pricing.Plans))
for i, item := range plans {
if i >= len(config.Pricing.Plans) {
break
}
if m, ok := item.(map[string]any); ok {
view.PlanNames[i] = overlayString(m, "name")
view.PlanPeriods[i] = overlayString(m, "period")
view.PlanCTAs[i] = overlayString(m, "cta")
}
}
} else {
view.PlanNames = make([]string, len(config.Pricing.Plans))
view.PlanPeriods = make([]string, len(config.Pricing.Plans))
view.PlanCTAs = make([]string, len(config.Pricing.Plans))
}
} else {
view.PlanNames = make([]string, len(config.Pricing.Plans))
view.PlanPeriods = make([]string, len(config.Pricing.Plans))
view.PlanCTAs = make([]string, len(config.Pricing.Plans))
}
// CTA Section
if ctaSec, ok := overlay["cta_section"].(map[string]any); ok {
if v, ok := ctaSec["headline"].(string); ok {
view.CTAHeadline = v
}
if v, ok := ctaSec["subheadline"].(string); ok {
view.CTASubheadline = v
}
view.CTAHeadline = overlayString(ctaSec, "headline")
view.CTASubheadline = overlayString(ctaSec, "subheadline")
if btn, ok := ctaSec["button"].(map[string]any); ok {
if v, ok := btn["label"].(string); ok {
view.CTAButton = v
view.CTAButton = overlayString(btn, "label")
}
}
// Blog
if blog, ok := overlay["blog"].(map[string]any); ok {
view.BlogHeadline = overlayString(blog, "headline")
view.BlogSubheadline = overlayString(blog, "subheadline")
if btn, ok := blog["cta_button"].(map[string]any); ok {
view.BlogCTAButton = overlayString(btn, "label")
}
}
// Gallery
if gallery, ok := overlay["gallery"].(map[string]any); ok {
view.GalleryHeadline = overlayString(gallery, "headline")
view.GallerySubheadline = overlayString(gallery, "subheadline")
}
// Comparison
if comp, ok := overlay["comparison"].(map[string]any); ok {
view.ComparisonHeadline = overlayString(comp, "headline")
view.ComparisonSubheadline = overlayString(comp, "subheadline")
}
// Cross-Promote
if cp, ok := overlay["cross_promote"].(map[string]any); ok {
view.CrossPromoteHeadline = overlayString(cp, "headline")
view.CrossPromoteSubheadline = overlayString(cp, "subheadline")
}
// Footer
view.FooterLinkLabels = make([]string, len(config.Footer.Links))
if footer, ok := overlay["footer"].(map[string]any); ok {
view.FooterCopyright = overlayString(footer, "copyright")
if links, ok := footer["links"].([]any); ok {
for i, item := range links {
if i >= len(config.Footer.Links) {
break
}
if m, ok := item.(map[string]any); ok {
view.FooterLinkLabels[i] = overlayString(m, "label")
}
}
}
}
// SEO
if seo, ok := overlay["seo"].(map[string]any); ok {
view.SEOTitle = overlayString(seo, "title")
view.SEODescription = overlayString(seo, "description")
}
// Navigation labels
if nav, ok := overlay["navigation"].(map[string]any); ok {
view.NavLabelValueProps = overlayString(nav, "label_value_props")
view.NavLabelFeatures = overlayString(nav, "label_features")
view.NavLabelPricing = overlayString(nav, "label_pricing")
view.NavLabelBlog = overlayString(nav, "label_blog")
view.NavLabelGallery = overlayString(nav, "label_gallery")
view.NavLabelCompare = overlayString(nav, "label_compare")
view.NavLabelCrossPromote = overlayString(nav, "label_cross_promote")
view.NavLabelDocs = overlayString(nav, "label_docs")
view.NavLabelReleases = overlayString(nav, "label_releases")
view.NavLabelAPI = overlayString(nav, "label_api")
view.NavLabelIssues = overlayString(nav, "label_issues")
}
return view
}
@@ -776,6 +1038,19 @@ func parseTranslationView(t *pages_model.Translation) *TranslationView {
func buildTranslationJSON(ctx *context.Context) string {
overlay := map[string]any{}
// Brand
brand := map[string]any{}
if v := ctx.FormString("trans_brand_name"); v != "" {
brand["name"] = v
}
if v := ctx.FormString("trans_brand_tagline"); v != "" {
brand["tagline"] = v
}
if len(brand) > 0 {
overlay["brand"] = brand
}
// Hero
hero := map[string]any{}
if v := ctx.FormString("trans_headline"); v != "" {
hero["headline"] = v
@@ -793,6 +1068,115 @@ func buildTranslationJSON(ctx *context.Context) string {
overlay["hero"] = hero
}
// Stats (indexed)
var stats []map[string]any
for i := range 20 {
v := ctx.FormString(fmt.Sprintf("trans_stat_%d_value", i))
l := ctx.FormString(fmt.Sprintf("trans_stat_%d_label", i))
if v == "" && l == "" {
// Check if there are more by looking ahead
if ctx.FormString(fmt.Sprintf("trans_stat_%d_value", i+1)) == "" &&
ctx.FormString(fmt.Sprintf("trans_stat_%d_label", i+1)) == "" {
break
}
}
stats = append(stats, map[string]any{"value": v, "label": l})
}
if len(stats) > 0 {
overlay["stats"] = stats
}
// Value Props (indexed)
var valueProps []map[string]any
for i := range 20 {
t := ctx.FormString(fmt.Sprintf("trans_vp_%d_title", i))
d := ctx.FormString(fmt.Sprintf("trans_vp_%d_desc", i))
if t == "" && d == "" {
if ctx.FormString(fmt.Sprintf("trans_vp_%d_title", i+1)) == "" &&
ctx.FormString(fmt.Sprintf("trans_vp_%d_desc", i+1)) == "" {
break
}
}
valueProps = append(valueProps, map[string]any{"title": t, "description": d})
}
if len(valueProps) > 0 {
overlay["value_props"] = valueProps
}
if v := ctx.FormString("trans_value_props_headline"); v != "" {
overlay["value_props_headline"] = v
}
if v := ctx.FormString("trans_value_props_subheadline"); v != "" {
overlay["value_props_subheadline"] = v
}
// Features (indexed)
var features []map[string]any
for i := range 20 {
t := ctx.FormString(fmt.Sprintf("trans_feat_%d_title", i))
d := ctx.FormString(fmt.Sprintf("trans_feat_%d_desc", i))
if t == "" && d == "" {
if ctx.FormString(fmt.Sprintf("trans_feat_%d_title", i+1)) == "" &&
ctx.FormString(fmt.Sprintf("trans_feat_%d_desc", i+1)) == "" {
break
}
}
features = append(features, map[string]any{"title": t, "description": d})
}
if len(features) > 0 {
overlay["features"] = features
}
if v := ctx.FormString("trans_features_headline"); v != "" {
overlay["features_headline"] = v
}
if v := ctx.FormString("trans_features_subheadline"); v != "" {
overlay["features_subheadline"] = v
}
// Testimonials (indexed)
var testimonials []map[string]any
for i := range 20 {
q := ctx.FormString(fmt.Sprintf("trans_test_%d_quote", i))
r := ctx.FormString(fmt.Sprintf("trans_test_%d_role", i))
if q == "" && r == "" {
if ctx.FormString(fmt.Sprintf("trans_test_%d_quote", i+1)) == "" &&
ctx.FormString(fmt.Sprintf("trans_test_%d_role", i+1)) == "" {
break
}
}
testimonials = append(testimonials, map[string]any{"quote": q, "role": r})
}
if len(testimonials) > 0 {
overlay["social_proof"] = map[string]any{"testimonials": testimonials}
}
// Pricing
pricing := map[string]any{}
if v := ctx.FormString("trans_pricing_headline"); v != "" {
pricing["headline"] = v
}
if v := ctx.FormString("trans_pricing_subheadline"); v != "" {
pricing["subheadline"] = v
}
var plans []map[string]any
for i := range 10 {
n := ctx.FormString(fmt.Sprintf("trans_plan_%d_name", i))
p := ctx.FormString(fmt.Sprintf("trans_plan_%d_period", i))
c := ctx.FormString(fmt.Sprintf("trans_plan_%d_cta", i))
if n == "" && p == "" && c == "" {
if ctx.FormString(fmt.Sprintf("trans_plan_%d_name", i+1)) == "" {
break
}
}
plans = append(plans, map[string]any{"name": n, "period": p, "cta": c})
}
if len(plans) > 0 {
pricing["plans"] = plans
}
if len(pricing) > 0 {
overlay["pricing"] = pricing
}
// CTA Section
ctaSec := map[string]any{}
if v := ctx.FormString("trans_cta_headline"); v != "" {
ctaSec["headline"] = v
@@ -807,6 +1191,106 @@ func buildTranslationJSON(ctx *context.Context) string {
overlay["cta_section"] = ctaSec
}
// Blog
blog := map[string]any{}
if v := ctx.FormString("trans_blog_headline"); v != "" {
blog["headline"] = v
}
if v := ctx.FormString("trans_blog_subheadline"); v != "" {
blog["subheadline"] = v
}
if v := ctx.FormString("trans_blog_cta"); v != "" {
blog["cta_button"] = map[string]any{"label": v}
}
if len(blog) > 0 {
overlay["blog"] = blog
}
// Gallery
gallery := map[string]any{}
if v := ctx.FormString("trans_gallery_headline"); v != "" {
gallery["headline"] = v
}
if v := ctx.FormString("trans_gallery_subheadline"); v != "" {
gallery["subheadline"] = v
}
if len(gallery) > 0 {
overlay["gallery"] = gallery
}
// Comparison
comp := map[string]any{}
if v := ctx.FormString("trans_comparison_headline"); v != "" {
comp["headline"] = v
}
if v := ctx.FormString("trans_comparison_subheadline"); v != "" {
comp["subheadline"] = v
}
if len(comp) > 0 {
overlay["comparison"] = comp
}
// Cross-Promote
cp := map[string]any{}
if v := ctx.FormString("trans_cross_promote_headline"); v != "" {
cp["headline"] = v
}
if v := ctx.FormString("trans_cross_promote_subheadline"); v != "" {
cp["subheadline"] = v
}
if len(cp) > 0 {
overlay["cross_promote"] = cp
}
// Footer
footer := map[string]any{}
if v := ctx.FormString("trans_footer_copyright"); v != "" {
footer["copyright"] = v
}
var footerLinks []map[string]any
for i := range 20 {
l := ctx.FormString(fmt.Sprintf("trans_footer_link_%d", i))
if l == "" {
if ctx.FormString(fmt.Sprintf("trans_footer_link_%d", i+1)) == "" {
break
}
}
footerLinks = append(footerLinks, map[string]any{"label": l})
}
if len(footerLinks) > 0 {
footer["links"] = footerLinks
}
if len(footer) > 0 {
overlay["footer"] = footer
}
// SEO
seo := map[string]any{}
if v := ctx.FormString("trans_seo_title"); v != "" {
seo["title"] = v
}
if v := ctx.FormString("trans_seo_description"); v != "" {
seo["description"] = v
}
if len(seo) > 0 {
overlay["seo"] = seo
}
// Navigation labels
nav := map[string]any{}
for _, key := range []string{
"label_value_props", "label_features", "label_pricing",
"label_blog", "label_gallery", "label_compare", "label_cross_promote",
"label_docs", "label_releases", "label_api", "label_issues",
} {
if v := ctx.FormString("trans_nav_" + key); v != "" {
nav[key] = v
}
}
if len(nav) > 0 {
overlay["navigation"] = nav
}
if len(overlay) == 0 {
return ""
}
@@ -823,18 +1307,19 @@ func PagesLanguages(ctx *context.Context) {
config := getPagesLandingConfig(ctx)
ctx.Data["LanguageNames"] = pages_module.LanguageDisplayNames()
// Build a function to check if a language is enabled
enabledLangs := config.I18n.Languages
ctx.Data["IsLangEnabled"] = func(code string) bool {
return slices.Contains(enabledLangs, code)
// Build a map to check if a language is enabled
enabledLangsMap := make(map[string]bool)
for _, code := range config.I18n.Languages {
enabledLangsMap[code] = true
}
ctx.Data["EnabledLangs"] = enabledLangsMap
// Load translations into a map[lang]*TranslationView
translationMap := make(map[string]*TranslationView)
translations, err := pages_model.GetTranslationsByRepoID(ctx, ctx.Repo.Repository.ID)
if err == nil {
for _, t := range translations {
translationMap[t.Lang] = parseTranslationView(t)
translationMap[t.Lang] = parseTranslationView(t, config)
}
}
ctx.Data["TranslationMap"] = translationMap
@@ -953,6 +1438,73 @@ func PagesLanguagesPost(ctx *context.Context) {
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.ai_translate_success"))
case "ai_translate_all":
defaultLang := config.I18n.DefaultLang
if defaultLang == "" {
defaultLang = "en"
}
var (
mu sync.Mutex
successCount int
failCount int
wg sync.WaitGroup
)
repoID := ctx.Repo.Repository.ID
repo := ctx.Repo.Repository
for _, lang := range config.I18n.Languages {
if lang == defaultLang {
continue
}
wg.Add(1)
go func(lang string) {
defer wg.Done()
translated, err := pages_service.TranslateLandingPageContent(ctx, repo, config, lang)
if err != nil {
log.Error("AI translation failed for %s: %v", lang, err)
mu.Lock()
failCount++
mu.Unlock()
return
}
mu.Lock()
defer mu.Unlock()
existing, err := pages_model.GetTranslation(ctx, repoID, lang)
if err != nil {
log.Error("GetTranslation failed for %s: %v", lang, err)
failCount++
return
}
if existing != nil {
existing.ConfigJSON = translated
existing.AutoGenerated = true
if err := pages_model.UpdateTranslation(ctx, existing); err != nil {
log.Error("UpdateTranslation failed for %s: %v", lang, err)
failCount++
return
}
} else {
t := &pages_model.Translation{
RepoID: repoID,
Lang: lang,
ConfigJSON: translated,
AutoGenerated: true,
}
if err := pages_model.CreateTranslation(ctx, t); err != nil {
log.Error("CreateTranslation failed for %s: %v", lang, err)
failCount++
return
}
}
successCount++
}(lang)
}
wg.Wait()
if failCount == 0 {
ctx.Flash.Success(ctx.Tr("repo.settings.pages.ai_translate_all_success", successCount))
} else {
ctx.Flash.Warning(ctx.Tr("repo.settings.pages.ai_translate_all_partial", successCount, successCount+failCount, failCount))
}
}
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/languages")
@@ -992,3 +1544,64 @@ func loadRawReadme(ctx *context.Context, repo *repo_model.Repository) string {
}
return ""
}
// PagesAdvanced renders the advanced settings page
func PagesAdvanced(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.advanced")
ctx.Data["PageIsSettingsPages"] = true
ctx.Data["PageIsSettingsPagesAdvanced"] = true
setCommonPagesData(ctx)
ctx.HTML(http.StatusOK, tplRepoSettingsPagesAdvanced)
}
// PagesAdvancedPost handles the advanced settings form submission
func PagesAdvancedPost(ctx *context.Context) {
config := getPagesLandingConfig(ctx)
// Parse static routes
var routes []string
for i := range 100 {
route := strings.TrimSpace(ctx.FormString(fmt.Sprintf("static_route_%d", i)))
if route != "" {
routes = append(routes, route)
}
}
config.Advanced.StaticRoutes = routes
// Parse redirects
redirects := make(map[string]string)
for i := range 100 {
from := strings.TrimSpace(ctx.FormString(fmt.Sprintf("redirect_from_%d", i)))
to := strings.TrimSpace(ctx.FormString(fmt.Sprintf("redirect_to_%d", i)))
if from != "" && to != "" {
redirects[from] = to
}
}
// Also handle redirects keyed by path (from existing entries)
for key, vals := range ctx.Req.Form {
if strings.HasPrefix(key, "redirect_from_/") {
from := strings.TrimSpace(vals[0])
toKey := "redirect_to_" + strings.TrimPrefix(key, "redirect_from_")
to := strings.TrimSpace(ctx.Req.FormValue(toKey))
if from != "" && to != "" {
redirects[from] = to
}
}
}
if len(redirects) > 0 {
config.Advanced.Redirects = redirects
} else {
config.Advanced.Redirects = nil
}
// Parse remaining fields
config.Advanced.CustomCSS = ctx.FormString("custom_css")
config.Advanced.CustomHead = ctx.FormString("custom_head")
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/advanced")
}

View File

@@ -470,12 +470,13 @@ func handleSettingsPostPushMirrorAdd(ctx *context.Context) {
}
m := &repo_model.PushMirror{
RepoID: repo.ID,
Repo: repo,
RemoteName: "remote_mirror_" + remoteSuffix,
SyncOnCommit: form.PushMirrorSyncOnCommit,
Interval: interval,
RemoteAddress: remoteAddress,
RepoID: repo.ID,
Repo: repo,
RemoteName: "remote_mirror_" + remoteSuffix,
SyncOnCommit: form.PushMirrorSyncOnCommit,
ExcludeHiddenFiles: form.PushMirrorExcludeHiddenFiles,
Interval: interval,
RemoteAddress: remoteAddress,
}
if err := db.Insert(ctx, m); err != nil {
ctx.ServerError("InsertPushMirror", err)

View File

@@ -1347,6 +1347,7 @@ func registerWebRoutes(m *web.Router) {
m.Combo("/footer").Get(repo_setting.PagesFooter).Post(repo_setting.PagesFooterPost)
m.Combo("/theme").Get(repo_setting.PagesTheme).Post(repo_setting.PagesThemePost)
m.Combo("/languages").Get(repo_setting.PagesLanguages).Post(repo_setting.PagesLanguagesPost)
m.Combo("/advanced").Get(repo_setting.PagesAdvanced).Post(repo_setting.PagesAdvancedPost)
})
m.Group("/actions/general", func() {
m.Get("", repo_setting.ActionsGeneralSettings)

View File

@@ -89,26 +89,27 @@ func (f *MigrateRepoForm) Validate(req *http.Request, errs binding.Errors) bindi
// RepoSettingForm form for changing repository settings
type RepoSettingForm struct {
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
Description string `binding:"MaxSize(2048)"`
DisplayTitle string `binding:"MaxSize(255)"`
GroupHeader string `binding:"MaxSize(255)"`
OwnerDisplayName string `binding:"MaxSize(255)"`
Website string `binding:"ValidUrl;MaxSize(1024)"`
Interval string
MirrorAddress string
MirrorUsername string
MirrorPassword string
LFS bool `form:"mirror_lfs"`
LFSEndpoint string `form:"mirror_lfs_endpoint"`
PushMirrorID int64
PushMirrorAddress string
PushMirrorUsername string
PushMirrorPassword string
PushMirrorSyncOnCommit bool
PushMirrorInterval string
Template bool
EnablePrune bool
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
Description string `binding:"MaxSize(2048)"`
DisplayTitle string `binding:"MaxSize(255)"`
GroupHeader string `binding:"MaxSize(255)"`
OwnerDisplayName string `binding:"MaxSize(255)"`
Website string `binding:"ValidUrl;MaxSize(1024)"`
Interval string
MirrorAddress string
MirrorUsername string
MirrorPassword string
LFS bool `form:"mirror_lfs"`
LFSEndpoint string `form:"mirror_lfs_endpoint"`
PushMirrorID int64
PushMirrorAddress string
PushMirrorUsername string
PushMirrorPassword string
PushMirrorSyncOnCommit bool
PushMirrorExcludeHiddenFiles bool
PushMirrorInterval string
Template bool
EnablePrune bool
// Advanced settings
EnableCode bool

View File

@@ -28,6 +28,82 @@ import (
var stripExitStatus = regexp.MustCompile(`exit status \d+ - `)
// buildFilterOpts builds a FilterTreeOptions from the repo's hidden file settings.
func buildFilterOpts(ctx context.Context, repo *repo_model.Repository) *git.FilterTreeOptions {
opts := &git.FilterTreeOptions{
ExcludeDotFiles: repo.HideDotfiles,
}
if hiddenPaths, err := repo_model.GetHiddenFolderPaths(ctx, repo.ID); err == nil && len(hiddenPaths) > 0 {
opts.ExcludePaths = hiddenPaths
}
return opts
}
// performFilteredPush pushes each branch and tag with dot folders/hidden paths stripped.
// Uses git plumbing to build filtered commits (no refs created, no artifacts left behind).
func performFilteredPush(ctx context.Context, repo *repo_model.Repository, remoteName string, filterOpts *git.FilterTreeOptions, timeout time.Duration) error {
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
return fmt.Errorf("open repository: %w", err)
}
defer gitRepo.Close()
remoteURL, err := gitrepo.GitRemoteGetURL(ctx, repo, remoteName)
if err != nil {
return fmt.Errorf("get remote URL: %w", err)
}
envs := proxy.EnvWithProxy(remoteURL.URL)
// Push each branch with filtered content
branches, _, err := gitRepo.GetBranchNames(0, 0)
if err != nil {
return fmt.Errorf("get branches: %w", err)
}
for _, branch := range branches {
commitID, err := gitRepo.GetBranchCommitID(branch)
if err != nil {
log.Warn("GetBranchCommitID(%s): %v", branch, err)
continue
}
filteredSHA, err := gitRepo.BuildFilteredCommit(commitID, filterOpts)
if err != nil {
log.Warn("BuildFilteredCommit(%s/%s): %v", branch, commitID, err)
continue
}
log.Trace("Push mirror filtered [repo: %-v] branch %s: %s -> %s", repo, branch, commitID[:12], filteredSHA[:12])
if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{
Remote: remoteName,
Force: true,
Branch: "refs/heads/" + branch,
LocalRefName: filteredSHA,
Timeout: timeout,
Env: envs,
}); err != nil {
return fmt.Errorf("push branch %s: %w", branch, util.SanitizeErrorCredentialURLs(err))
}
}
// Push tags as-is (tags point at commits, not trees, so dot folders in the
// tagged tree are acceptable — the tag itself has no file content).
if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{
Remote: remoteName,
Force: true,
Branch: "refs/tags/*",
LocalRefName: "refs/tags/*",
Timeout: timeout,
Env: envs,
}); err != nil {
// Non-fatal — tags may not exist or remote may reject
log.Trace("Push mirror tags [repo: %-v]: %v", repo, err)
}
return nil
}
// AddPushMirrorRemote registers the push mirror remote.
func AddPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr string) error {
addRemoteAndConfig := func(storageRepo gitrepo.Repository, addr string) error {
@@ -168,6 +244,43 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
return nil
}
// Use filtered push if ExcludeHiddenFiles is enabled and the repo has hidden content
if m.ExcludeHiddenFiles {
filterOpts := buildFilterOpts(ctx, m.Repo)
if filterOpts.HasFilters() {
log.Trace("SyncPushMirror [mirror: %d][repo: %-v]: Using filtered push (exclude hidden files)", m.ID, m.Repo)
// LFS sync still happens normally
if setting.LFS.StartServer {
gitRepo, err := gitrepo.OpenRepository(ctx, m.Repo)
if err == nil {
remoteURL, err := gitrepo.GitRemoteGetURL(ctx, m.Repo, m.RemoteName)
if err == nil {
endpoint := lfs.DetermineEndpoint(remoteURL.String(), "")
lfsClient := lfs.NewClient(endpoint, nil)
if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil {
gitRepo.Close()
return util.SanitizeErrorCredentialURLs(err)
}
}
gitRepo.Close()
}
}
if err := performFilteredPush(ctx, m.Repo, m.RemoteName, filterOpts, timeout); err != nil {
return err
}
// Wiki is always pushed unfiltered (no dot folders concern)
if repo_service.HasWiki(ctx, m.Repo) {
if _, err := gitrepo.GitRemoteGetURL(ctx, m.Repo.WikiStorageRepo(), m.RemoteName); err == nil {
_ = performPush(m.Repo, true)
}
}
return nil
}
}
err := performPush(m.Repo, false)
if err != nil {
return err

218
services/org/profile.go Normal file
View File

@@ -0,0 +1,218 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"context"
"strings"
"code.gitcaddy.com/server/v3/models/db"
repo_model "code.gitcaddy.com/server/v3/models/repo"
user_model "code.gitcaddy.com/server/v3/models/user"
"code.gitcaddy.com/server/v3/modules/git"
repo_service "code.gitcaddy.com/server/v3/services/repository"
files_service "code.gitcaddy.com/server/v3/services/repository/files"
)
// GetOrgProfileReadme reads the README.md from the org's .profile repository.
// Returns empty string if the .profile repo doesn't exist or has no README.
func GetOrgProfileReadme(ctx context.Context, orgID int64) (string, error) {
profileRepo, err := repo_model.GetRepositoryByName(ctx, orgID, ".profile")
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
return "", nil
}
return "", err
}
if profileRepo.IsEmpty {
return "", nil
}
gitRepo, err := git.OpenRepository(ctx, profileRepo.RepoPath())
if err != nil {
return "", err
}
defer gitRepo.Close()
commit, err := gitRepo.GetBranchCommit(profileRepo.DefaultBranch)
if err != nil {
return "", err
}
// Try common README filenames
readmeFiles := []string{"README.md", "readme.md", "Readme.md", "README", "README.txt"}
for _, filename := range readmeFiles {
entry, err := commit.GetTreeEntryByPath(filename)
if err == nil && !entry.IsDir() {
content, err := entry.Blob().GetBlobContent(1024 * 512) // 512KB max
if err == nil {
return content, nil
}
}
}
return "", nil
}
// UpdateOrgProfileReadme updates (or creates) the README.md in the org's .profile repository.
// If the .profile repo doesn't exist, it is created first.
func UpdateOrgProfileReadme(ctx context.Context, doer *user_model.User, orgID int64, content, commitMessage string) error {
// Look up the org as a user (needed for repo operations)
orgUser, err := user_model.GetUserByID(ctx, orgID)
if err != nil {
return err
}
profileRepo, err := repo_model.GetRepositoryByName(ctx, orgID, ".profile")
if err != nil {
if !repo_model.IsErrRepoNotExist(err) {
return err
}
// Create .profile repo
profileRepo, err = repo_service.CreateRepository(ctx, doer, orgUser, repo_service.CreateRepoOptions{
Name: ".profile",
Description: "Organization profile",
AutoInit: true,
Readme: "Default",
DefaultBranch: "main",
IsPrivate: false,
})
if err != nil {
return err
}
}
if commitMessage == "" {
commitMessage = "Update organization profile README"
}
// Determine operation: create or update
operation := "update"
var existingSHA string
if !profileRepo.IsEmpty {
gitRepo, err := git.OpenRepository(ctx, profileRepo.RepoPath())
if err == nil {
commit, err := gitRepo.GetBranchCommit(profileRepo.DefaultBranch)
if err == nil {
entry, err := commit.GetTreeEntryByPath("README.md")
if err != nil {
operation = "create"
} else {
existingSHA = entry.ID.String()
}
}
gitRepo.Close()
}
} else {
operation = "create"
}
_, err = files_service.ChangeRepoFiles(ctx, profileRepo, doer, &files_service.ChangeRepoFilesOptions{
OldBranch: profileRepo.DefaultBranch,
NewBranch: profileRepo.DefaultBranch,
Message: commitMessage,
Files: []*files_service.ChangeRepoFile{
{
Operation: operation,
TreePath: "README.md",
ContentReader: strings.NewReader(content),
SHA: existingSHA,
},
},
Author: &files_service.IdentityOptions{
GitUserName: doer.Name,
GitUserEmail: doer.Email,
},
})
// If update failed because file doesn't exist, try create
if err != nil && operation == "update" {
_, err = files_service.ChangeRepoFiles(ctx, profileRepo, doer, &files_service.ChangeRepoFilesOptions{
OldBranch: profileRepo.DefaultBranch,
NewBranch: profileRepo.DefaultBranch,
Message: commitMessage,
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "README.md",
ContentReader: strings.NewReader(content),
},
},
Author: &files_service.IdentityOptions{
GitUserName: doer.Name,
GitUserEmail: doer.Email,
},
})
}
return err
}
// GetOrgProfileRepo returns the .profile repository for an org, or nil if it doesn't exist.
func GetOrgProfileRepo(ctx context.Context, orgID int64) (*repo_model.Repository, error) {
repo, err := repo_model.GetRepositoryByName(ctx, orgID, ".profile")
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
return nil, nil
}
return nil, err
}
return repo, nil
}
// GetOrgRecentActivity returns the N most recently updated repos for an org
// with their latest commit info.
type RecentRepoActivity struct {
RepoName string `json:"repo_name"`
RepoFullName string `json:"repo_full_name"`
DefaultBranch string `json:"default_branch"`
CommitMessage string `json:"commit_message,omitempty"`
CommitTime int64 `json:"commit_time,omitempty"`
IsPrivate bool `json:"is_private"`
}
func GetOrgRecentActivity(ctx context.Context, orgID int64, actor *user_model.User, limit int) ([]*RecentRepoActivity, error) {
if limit <= 0 {
limit = 10
}
showPrivate := actor != nil
repos, _, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{PageSize: limit, Page: 1},
OwnerID: orgID,
OrderBy: db.SearchOrderByRecentUpdated,
Private: showPrivate,
Actor: actor,
})
if err != nil {
return nil, err
}
result := make([]*RecentRepoActivity, 0, len(repos))
for _, repo := range repos {
activity := &RecentRepoActivity{
RepoName: repo.Name,
RepoFullName: repo.FullName(),
DefaultBranch: repo.DefaultBranch,
IsPrivate: repo.IsPrivate,
}
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
if err == nil {
commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
if err == nil {
activity.CommitMessage = commit.Summary()
activity.CommitTime = commit.Author.When.Unix()
}
gitRepo.Close()
}
result = append(result, activity)
}
return result, nil
}

View File

@@ -209,35 +209,275 @@ func TranslateLandingPageContent(ctx context.Context, repo *repo_model.Repositor
// Validate it's valid JSON
result := extractJSON(resp.Result)
var check map[string]any
if err := json.Unmarshal([]byte(result), &check); err != nil {
var parsed map[string]any
if err := json.Unmarshal([]byte(result), &parsed); err != nil {
return "", fmt.Errorf("AI returned invalid JSON: %w", err)
}
return result, nil
// Strip placeholder values the AI sometimes returns for empty inputs
// (e.g. "<UNKNOWN>", "<EMPTY>", "N/A") so they don't end up saved as translations.
stripPlaceholders(parsed)
cleaned, err := json.Marshal(parsed)
if err != nil {
return "", fmt.Errorf("re-marshal translation: %w", err)
}
return string(cleaned), nil
}
// stripPlaceholders walks a translation overlay and removes string values that
// look like AI placeholders for empty input (e.g. "<UNKNOWN>", "<EMPTY>", "N/A").
// This keeps stale "<UNKNOWN>" strings out of the saved translation overlay.
func stripPlaceholders(v any) {
switch t := v.(type) {
case map[string]any:
for k, child := range t {
if s, ok := child.(string); ok && isPlaceholder(s) {
delete(t, k)
continue
}
stripPlaceholders(child)
}
case []any:
for _, item := range t {
stripPlaceholders(item)
}
}
}
func isPlaceholder(s string) bool {
switch strings.TrimSpace(strings.ToUpper(s)) {
case "<UNKNOWN>", "<EMPTY>", "<NULL>", "N/A", "NONE":
return true
}
return false
}
// setIfNotEmpty assigns v to m[key] only if v is non-empty.
func setIfNotEmpty(m map[string]any, key, v string) {
if v != "" {
m[key] = v
}
}
// buildTranslatableContent extracts translatable text from a config for the AI
func buildTranslatableContent(config *pages_module.LandingConfig) string {
data, _ := json.Marshal(map[string]any{
"hero": map[string]any{
"headline": config.Hero.Headline,
"subheadline": config.Hero.Subheadline,
"primary_cta": map[string]string{"label": config.Hero.PrimaryCTA.Label},
"secondary_cta": map[string]string{"label": config.Hero.SecondaryCTA.Label},
},
"stats": config.Stats,
"value_props": config.ValueProps,
"features": config.Features,
"cta_section": map[string]any{
"headline": config.CTASection.Headline,
"subheadline": config.CTASection.Subheadline,
"button": map[string]string{"label": config.CTASection.Button.Label},
},
"seo": map[string]any{
"title": config.SEO.Title,
"description": config.SEO.Description,
},
})
content := map[string]any{}
// Brand
if config.Brand.Name != "" || config.Brand.Tagline != "" {
brand := map[string]any{}
setIfNotEmpty(brand, "name", config.Brand.Name)
setIfNotEmpty(brand, "tagline", config.Brand.Tagline)
if len(brand) > 0 {
content["brand"] = brand
}
}
// Hero — only include non-empty fields so the AI doesn't return "<UNKNOWN>"
hero := map[string]any{}
setIfNotEmpty(hero, "headline", config.Hero.Headline)
setIfNotEmpty(hero, "subheadline", config.Hero.Subheadline)
if config.Hero.PrimaryCTA.Label != "" {
hero["primary_cta"] = map[string]string{"label": config.Hero.PrimaryCTA.Label}
}
if config.Hero.SecondaryCTA.Label != "" {
hero["secondary_cta"] = map[string]string{"label": config.Hero.SecondaryCTA.Label}
}
if len(hero) > 0 {
content["hero"] = hero
}
// Stats, Value Props, Features (already struct-serializable)
if len(config.Stats) > 0 {
content["stats"] = config.Stats
}
if len(config.ValueProps) > 0 {
content["value_props"] = config.ValueProps
}
if config.ValuePropsHeadline != "" {
content["value_props_headline"] = config.ValuePropsHeadline
}
if config.ValuePropsSubheadline != "" {
content["value_props_subheadline"] = config.ValuePropsSubheadline
}
if len(config.Features) > 0 {
content["features"] = config.Features
}
if config.FeaturesHeadline != "" {
content["features_headline"] = config.FeaturesHeadline
}
if config.FeaturesSubheadline != "" {
content["features_subheadline"] = config.FeaturesSubheadline
}
// Testimonials
if len(config.SocialProof.Testimonials) > 0 {
testimonials := make([]map[string]string, 0, len(config.SocialProof.Testimonials))
for _, t := range config.SocialProof.Testimonials {
tm := map[string]string{}
if t.Quote != "" {
tm["quote"] = t.Quote
}
if t.Role != "" {
tm["role"] = t.Role
}
if len(tm) > 0 {
testimonials = append(testimonials, tm)
}
}
if len(testimonials) > 0 {
content["social_proof"] = map[string]any{"testimonials": testimonials}
}
}
// Pricing
if config.Pricing.Headline != "" || len(config.Pricing.Plans) > 0 {
pricing := map[string]any{}
setIfNotEmpty(pricing, "headline", config.Pricing.Headline)
setIfNotEmpty(pricing, "subheadline", config.Pricing.Subheadline)
if len(config.Pricing.Plans) > 0 {
plans := make([]map[string]string, 0, len(config.Pricing.Plans))
for _, p := range config.Pricing.Plans {
plan := map[string]string{}
if p.Name != "" {
plan["name"] = p.Name
}
if p.Period != "" {
plan["period"] = p.Period
}
if p.CTA != "" {
plan["cta"] = p.CTA
}
plans = append(plans, plan)
}
pricing["plans"] = plans
}
if len(pricing) > 0 {
content["pricing"] = pricing
}
}
// CTA Section
cta := map[string]any{}
setIfNotEmpty(cta, "headline", config.CTASection.Headline)
setIfNotEmpty(cta, "subheadline", config.CTASection.Subheadline)
if config.CTASection.Button.Label != "" {
cta["button"] = map[string]string{"label": config.CTASection.Button.Label}
}
if len(cta) > 0 {
content["cta_section"] = cta
}
// Blog
if config.Blog.Enabled {
blogHeadline := config.Blog.Headline
if blogHeadline == "" {
blogHeadline = "Latest Posts"
}
blog := map[string]any{
"headline": blogHeadline,
}
setIfNotEmpty(blog, "subheadline", config.Blog.Subheadline)
if config.Blog.CTAButton.Label != "" {
blog["cta_button"] = map[string]string{"label": config.Blog.CTAButton.Label}
}
content["blog"] = blog
}
// Gallery
if config.Gallery.Enabled {
galleryHeadline := config.Gallery.Headline
if galleryHeadline == "" {
galleryHeadline = "Gallery"
}
gallery := map[string]any{
"headline": galleryHeadline,
}
setIfNotEmpty(gallery, "subheadline", config.Gallery.Subheadline)
content["gallery"] = gallery
}
// Comparison
if config.Comparison.Enabled {
compHeadline := config.Comparison.Headline
if compHeadline == "" {
compHeadline = "How We Compare"
}
comparison := map[string]any{
"headline": compHeadline,
}
setIfNotEmpty(comparison, "subheadline", config.Comparison.Subheadline)
content["comparison"] = comparison
}
// Cross-Promote
if config.CrossPromote.Enabled {
cpHeadline := config.CrossPromote.Headline
if cpHeadline == "" {
cpHeadline = "Related Offerings"
}
crossPromote := map[string]any{
"headline": cpHeadline,
}
setIfNotEmpty(crossPromote, "subheadline", config.CrossPromote.Subheadline)
content["cross_promote"] = crossPromote
}
// Footer
if config.Footer.Copyright != "" || len(config.Footer.Links) > 0 {
footer := map[string]any{}
setIfNotEmpty(footer, "copyright", config.Footer.Copyright)
if len(config.Footer.Links) > 0 {
links := make([]map[string]string, 0, len(config.Footer.Links))
for _, l := range config.Footer.Links {
if l.Label != "" {
links = append(links, map[string]string{"label": l.Label})
}
}
if len(links) > 0 {
footer["links"] = links
}
}
if len(footer) > 0 {
content["footer"] = footer
}
}
// SEO
if config.SEO.Title != "" || config.SEO.Description != "" {
seo := map[string]any{}
setIfNotEmpty(seo, "title", config.SEO.Title)
setIfNotEmpty(seo, "description", config.SEO.Description)
if len(seo) > 0 {
content["seo"] = seo
}
}
// Navigation labels (for translating nav items and section headers)
// Use template-specific defaults so AI translates the correct terms
// (e.g. "Systems Analysis" for architecture-deep-dive, not generic "Value Props")
defaults := pages_module.TemplateDefaultLabels(config.Template)
labelOrDefault := func(label, def string) string {
if label != "" {
return label
}
return def
}
content["navigation"] = map[string]any{
"label_value_props": labelOrDefault(config.Navigation.LabelValueProps, defaults.LabelValueProps),
"label_features": labelOrDefault(config.Navigation.LabelFeatures, defaults.LabelFeatures),
"label_pricing": labelOrDefault(config.Navigation.LabelPricing, defaults.LabelPricing),
"label_blog": labelOrDefault(config.Navigation.LabelBlog, defaults.LabelBlog),
"label_gallery": labelOrDefault(config.Navigation.LabelGallery, defaults.LabelGallery),
"label_compare": labelOrDefault(config.Navigation.LabelCompare, defaults.LabelCompare),
"label_cross_promote": labelOrDefault(config.Navigation.LabelCrossPromote, defaults.LabelCrossPromote),
"label_docs": labelOrDefault(config.Navigation.LabelDocs, "Docs"),
"label_releases": labelOrDefault(config.Navigation.LabelReleases, "Releases"),
"label_api": labelOrDefault(config.Navigation.LabelAPI, "API"),
"label_issues": labelOrDefault(config.Navigation.LabelIssues, "Issues"),
}
data, _ := json.Marshal(content)
return string(data)
}

View File

@@ -173,6 +173,20 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
return nil, fmt.Errorf("createDelegateHooks: %w", err)
}
// 5b - Filter hidden files from fork if source repo has HideDotfiles or hidden folders
if opts.BaseRepo.HideDotfiles {
filterOpts := &git.FilterTreeOptions{ExcludeDotFiles: true}
if hiddenPaths, pathErr := repo_model.GetHiddenFolderPaths(ctx, opts.BaseRepo.ID); pathErr == nil && len(hiddenPaths) > 0 {
filterOpts.ExcludePaths = hiddenPaths
}
if filterOpts.HasFilters() {
if filterErr := filterForkBranches(ctx, repo, filterOpts); filterErr != nil {
log.Error("filterForkBranches failed for %v: %v", repo, filterErr)
// Non-fatal — fork still works, just has dot folders
}
}
}
// 6 - Sync the repository branches and tags
var gitRepo *git.Repository
gitRepo, err = gitrepo.OpenRepository(ctx, repo)
@@ -213,6 +227,47 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
return repo, nil
}
// filterForkBranches rewrites branch heads to exclude hidden/dot files.
// For each branch, it builds a filtered commit (no refs left behind) and
// updates the branch ref to point at the filtered commit.
func filterForkBranches(ctx context.Context, repo *repo_model.Repository, filterOpts *git.FilterTreeOptions) error {
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
return fmt.Errorf("open repository: %w", err)
}
defer gitRepo.Close()
branches, _, err := gitRepo.GetBranchNames(0, 0)
if err != nil {
return fmt.Errorf("get branches: %w", err)
}
for _, branch := range branches {
commitID, err := gitRepo.GetBranchCommitID(branch)
if err != nil {
log.Warn("filterForkBranches: GetBranchCommitID(%s): %v", branch, err)
continue
}
filteredSHA, err := gitRepo.BuildFilteredCommit(commitID, filterOpts)
if err != nil {
log.Warn("filterForkBranches: BuildFilteredCommit(%s): %v", branch, err)
continue
}
if filteredSHA == commitID {
continue // nothing filtered
}
// Update the branch ref to point at the filtered commit
if err := gitRepo.UpdateRef("refs/heads/"+branch, filteredSHA); err != nil {
log.Warn("filterForkBranches: update-ref(%s): %v", branch, err)
}
}
return nil
}
// ConvertForkToNormalRepository convert the provided repo from a forked repo to normal repo
func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Repository) error {
return db.WithTx(ctx, func(ctx context.Context) error {

View File

@@ -966,7 +966,19 @@
/* Responsive */
@media (max-width: 768px) {
.ad-nav { padding: 0 20px; }
.ad-nav-links { display: none; }
.ad-nav-links {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
padding: 16px 24px;
gap: 8px;
background: inherit;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 100;
}
.ad-menu-toggle { display: flex; }
.ad-hero { padding: 120px 24px 80px; }
.ad-hero-ctas { flex-direction: column; align-items: center; }
@@ -1006,16 +1018,17 @@
{{range .Config.Footer.Links}}
<a href="{{.URL}}" class="ad-nav-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="ad-nav-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="ad-nav-link">API</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ad-nav-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ad-nav-link">Issues</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="ad-nav-link">Value Props</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="ad-nav-link">Features</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="ad-nav-link">Pricing</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="ad-nav-link">Blog</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="ad-nav-link">Gallery</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="ad-nav-link">Compare</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="ad-nav-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="ad-nav-link">{{if .Config.Navigation.LabelAPI}}{{.Config.Navigation.LabelAPI}}{{else}}API{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ad-nav-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ad-nav-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="ad-nav-link">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Value Props{{end}}</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="ad-nav-link">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="ad-nav-link">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="ad-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="ad-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="ad-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="ad-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
{{if .Config.Navigation.ShowRepository}}
<a href="{{.RepoURL}}" class="ad-nav-cta">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
@@ -1071,7 +1084,7 @@
<section class="ad-features" style="padding-top: 120px;">
<div class="ad-features-inner">
<div class="ad-section-header ad-reveal">
<div class="ad-section-label">Blog</div>
<div class="ad-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}All Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1102,7 +1115,7 @@
</section>
{{else}}
<!-- Hero Section -->
<section class="ad-hero">
<section class="ad-hero" id="hero">
<div class="ad-hero-content">
<div class="ad-badge ad-reveal visible">
<span class="ad-badge-dot"></span>
@@ -1123,11 +1136,6 @@
{{.Config.Hero.PrimaryCTA.Label}}
{{svg "octicon-arrow-right" 16}}
</a>
{{else}}
<a href="{{.RepoURL}}" class="ad-btn-primary" data-cta="primary">
Get Started
{{svg "octicon-arrow-right" 16}}
</a>
{{end}}
{{if .Config.Hero.SecondaryCTA.Label}}
@@ -1135,14 +1143,26 @@
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
{{.Config.Hero.SecondaryCTA.Label}}
</a>
{{else}}
<a href="{{.RepoURL}}" class="ad-btn-secondary" data-cta="secondary">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
View Source
</a>
{{end}}
</div>
{{if or $.GooglePlayID $.AppStoreID}}
<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 16px;" class="ad-reveal visible ad-reveal-delay-4">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="ad-download-item" style="display: inline-flex; align-items: center; gap: 10px; clip-path: none;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="ad-download-item" style="display: inline-flex; align-items: center; gap: 10px; clip-path: none;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
{{end}}
{{if .Config.Hero.CodeExample}}
<div class="ad-code-block ad-reveal visible ad-reveal-delay-4">
<span class="ad-code-prompt">$</span>
@@ -1157,7 +1177,7 @@
<!-- Stats Section -->
{{if or .Config.Stats (gt .NumStars 0)}}
<section class="ad-stats">
<section class="ad-stats" id="stats">
<div class="ad-stats-inner ad-reveal">
{{if .Config.Stats}}
{{range .Config.Stats}}
@@ -1188,7 +1208,7 @@
<!-- Downloads Section -->
{{if and .PublicReleases .LatestRelease .LatestRelease.Attachments}}
<section class="ad-downloads">
<section class="ad-downloads" id="downloads">
<div class="ad-downloads-inner ad-reveal">
<h2>Download v{{.LatestReleaseTag}}</h2>
<p>Get the latest release</p>
@@ -1251,25 +1271,6 @@
</div>
{{end}}
{{end}}
{{if or $.GooglePlayID $.AppStoreID}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 13px; color: var(--ad-muted); font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.12em; text-transform: uppercase;">{{svg "octicon-device-mobile" 16}} App Stores</h4>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="ad-download-item" style="display: inline-flex; align-items: center; gap: 10px; clip-path: none;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="ad-download-item" style="display: inline-flex; align-items: center; gap: 10px; clip-path: none;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
</div>
{{end}}
{{if $otherFiles}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 13px; color: var(--ad-muted); font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.12em; text-transform: uppercase;">{{svg "octicon-file" 16}} Other</h4>
@@ -1287,10 +1288,10 @@
<section class="ad-features" id="value-props">
<div class="ad-features-inner">
<div class="ad-section-header ad-reveal">
<div class="ad-section-label">Systems Analysis</div>
<h2>{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}</h2>
<p>Everything you need to get started quickly.</p>
</div>
<div class="ad-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Systems Analysis{{end}}</div>
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
</div>
<div class="ad-features-grid">
{{range .Config.ValueProps}}
<div class="ad-feature-card ad-reveal">
@@ -1311,10 +1312,10 @@
<section class="ad-features" id="features" style="padding-top: 40px;">
<div class="ad-features-inner">
<div class="ad-section-header ad-reveal">
<div class="ad-section-label">Technical Specifications</div>
<h2>Features</h2>
<p>Powerful capabilities at your fingertips.</p>
</div>
<div class="ad-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Technical Specifications{{end}}</div>
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
</div>
<div class="ad-features-grid">
{{range .Config.Features}}
<div class="ad-feature-card ad-reveal">
@@ -1332,7 +1333,7 @@
<!-- Social Proof -->
{{if or .Config.SocialProof.Logos .Config.SocialProof.Testimonials}}
<section class="ad-social-proof">
<section class="ad-social-proof" id="social-proof">
<div class="ad-social-proof-inner">
{{if .Config.SocialProof.Logos}}
<div class="ad-logos ad-reveal">
@@ -1372,7 +1373,7 @@
<section class="ad-pricing" id="pricing">
<div class="ad-pricing-inner">
<div class="ad-section-header ad-reveal">
<div class="ad-section-label">Resource Allocation</div>
<div class="ad-section-label">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Resource Allocation{{end}}</div>
<h2>{{if .Config.Pricing.Headline}}{{.Config.Pricing.Headline}}{{else}}Pricing{{end}}</h2>
<p>{{if .Config.Pricing.Subheadline}}{{.Config.Pricing.Subheadline}}{{else}}Choose the plan that works for you{{end}}</p>
</div>
@@ -1396,9 +1397,35 @@
</section>
{{end}}
<!-- Cross-Promote Section -->
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
<section class="ad-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
<div class="ad-features-inner">
<div class="ad-section-header ad-reveal">
<div class="ad-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
</div>
<div class="ad-features-grid">
{{range .CrossPromoteItems}}
<a href="{{.URL}}" class="ad-feature-card ad-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
{{if .LogoURL}}
<div style="margin-bottom: 16px;">
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
</div>
{{end}}
<h3 class="ad-feature-title">{{.Name}}</h3>
{{if .Description}}<p class="ad-feature-desc">{{.Description}}</p>{{end}}
</a>
{{end}}
</div>
</div>
</section>
{{end}}
<!-- CTA Section -->
{{if .Config.CTASection.Headline}}
<section class="ad-cta-section">
<section class="ad-cta-section" id="cta">
<div class="ad-cta-inner ad-reveal">
<h2>{{.Config.CTASection.Headline}}</h2>
{{if .Config.CTASection.Subheadline}}
@@ -1417,7 +1444,7 @@
<section class="ad-features" id="blog" style="border-top: 1px solid var(--ad-dim);">
<div class="ad-features-inner">
<div class="ad-section-header ad-reveal">
<div class="ad-section-label">Dispatches</div>
<div class="ad-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Dispatches{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}Latest Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1456,7 +1483,7 @@
<section class="ad-features" id="gallery" style="border-top: 1px solid var(--ad-dim);">
<div class="ad-features-inner">
<div class="ad-section-header ad-reveal">
<div class="ad-section-label">Visual Index</div>
<div class="ad-section-label">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Visual Index{{end}}</div>
<h2>{{if .Config.Gallery.Headline}}{{.Config.Gallery.Headline}}{{else}}Gallery{{end}}</h2>
{{if .Config.Gallery.Subheadline}}<p>{{.Config.Gallery.Subheadline}}</p>{{end}}
</div>
@@ -1481,7 +1508,7 @@
<section class="ad-features" id="comparison" style="border-top: 1px solid var(--ad-dim);">
<div class="ad-features-inner">
<div class="ad-section-header ad-reveal">
<div class="ad-section-label">Compare</div>
<div class="ad-section-label">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</div>
<h2>{{if .Config.Comparison.Headline}}{{.Config.Comparison.Headline}}{{else}}How We Compare{{end}}</h2>
{{if .Config.Comparison.Subheadline}}<p>{{.Config.Comparison.Subheadline}}</p>{{end}}
</div>
@@ -1564,8 +1591,8 @@
{{end}}
{{if .Config.Navigation.ShowRepository}}<a href="{{.RepoURL}}" class="ad-footer-link">Repository</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="ad-footer-link">Documentation</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ad-footer-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ad-footer-link">Issues</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ad-footer-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ad-footer-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
</div>
</footer>
</div>

View File

@@ -28,6 +28,7 @@
.nb-reveal-delay-1 { transition-delay: 0.1s; }
.nb-reveal-delay-2 { transition-delay: 0.2s; }
.nb-reveal-delay-3 { transition-delay: 0.3s; }
.nb-reveal-delay-4 { transition-delay: 0.4s; }
.nb-page {
min-height: 100vh;
@@ -1064,7 +1065,19 @@
@media (max-width: 768px) {
.nb-nav { padding: 14px 24px; }
.nb-nav-links { display: none; }
.nb-nav-links {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
padding: 16px 24px;
gap: 8px;
background: inherit;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 100;
}
.nb-mobile-toggle { display: block; }
.nb-mobile-menu { padding: 24px; }
.nb-hero { padding: 120px 24px 80px; }
@@ -1128,16 +1141,17 @@
{{range .Config.Footer.Links}}
<a href="{{.URL}}" class="nb-nav-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="nb-nav-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="nb-nav-link">API</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="nb-nav-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="nb-nav-link">Issues</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="nb-nav-link">Why</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="nb-nav-link">Features</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="nb-nav-link">Pricing</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="nb-nav-link">Blog</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="nb-nav-link">Gallery</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="nb-nav-link">Compare</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="nb-nav-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="nb-nav-link">{{if .Config.Navigation.LabelAPI}}{{.Config.Navigation.LabelAPI}}{{else}}API{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="nb-nav-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="nb-nav-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="nb-nav-link">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why{{end}}</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="nb-nav-link">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="nb-nav-link">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="nb-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="nb-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="nb-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="nb-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
{{if .Config.Navigation.ShowRepository}}
<a href="{{.RepoURL}}" class="nb-nav-repo">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
@@ -1168,16 +1182,17 @@
{{range .Config.Footer.Links}}
<a href="{{.URL}}" class="nb-nav-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="nb-nav-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="nb-nav-link">API</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="nb-nav-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="nb-nav-link">Issues</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="nb-nav-link">Why</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="nb-nav-link">Features</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="nb-nav-link">Pricing</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="nb-nav-link">Blog</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="nb-nav-link">Gallery</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="nb-nav-link">Compare</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="nb-nav-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="nb-nav-link">{{if .Config.Navigation.LabelAPI}}{{.Config.Navigation.LabelAPI}}{{else}}API{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="nb-nav-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="nb-nav-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="nb-nav-link">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why{{end}}</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="nb-nav-link">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="nb-nav-link">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="nb-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="nb-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="nb-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="nb-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
{{if .Config.Navigation.ShowRepository}}
<a href="{{.RepoURL}}" class="nb-nav-repo">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
@@ -1217,7 +1232,7 @@
<section class="nb-features" style="padding-top: 120px;">
<div class="nb-features-inner">
<div class="nb-section-header nb-reveal">
<div class="nb-section-label">Blog</div>
<div class="nb-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}All Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1248,7 +1263,7 @@
</section>
{{else}}
<!-- Hero Section -->
<section class="nb-hero">
<section class="nb-hero" id="hero">
<div class="nb-hero-accent nb-hero-accent-1"></div>
<div class="nb-hero-accent nb-hero-accent-2"></div>
<div class="nb-hero-accent nb-hero-accent-3">*</div>
@@ -1273,12 +1288,29 @@
</a>
{{end}}
</div>
{{if or $.GooglePlayID $.AppStoreID}}
<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 16px;" class="nb-reveal nb-reveal-delay-4">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="nb-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="nb-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
{{end}}
</div>
</section>
{{if .Config.Stats}}
<!-- Stats Section -->
<section class="nb-stats">
<section class="nb-stats" id="stats">
<div class="nb-stats-grid">
{{range .Config.Stats}}
<div class="nb-stat-card nb-reveal">
@@ -1291,7 +1323,7 @@
{{end}}
{{if and .PublicReleases .LatestRelease .LatestRelease.Attachments}}
<section class="nb-downloads">
<section class="nb-downloads" id="downloads">
<div class="nb-downloads-inner">
<div class="nb-downloads-header nb-reveal">
<div class="nb-section-label">Download</div>
@@ -1357,25 +1389,6 @@
</div>
{{end}}
{{end}}
{{if or $.GooglePlayID $.AppStoreID}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 13px; color: var(--nb-muted); text-transform: uppercase; letter-spacing: 0.1em;">{{svg "octicon-device-mobile" 16}} App Stores</h4>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="nb-download-item nb-reveal" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="nb-download-item nb-reveal" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
</div>
{{end}}
{{if $otherFiles}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 13px; color: var(--nb-muted); text-transform: uppercase; letter-spacing: 0.1em;">{{svg "octicon-file" 16}} Other</h4>
@@ -1390,7 +1403,7 @@
{{if .Config.SocialProof.Logos}}
<!-- Trust Bar -->
<section class="nb-trust-bar">
<section class="nb-trust-bar" id="social-proof">
<p class="nb-trust-label nb-reveal">Trusted by the best</p>
<div class="nb-trust-logos nb-reveal nb-reveal-delay-1">
{{range .Config.SocialProof.Logos}}
@@ -1404,10 +1417,10 @@
<!-- Value Props Section -->
<section class="nb-value-props" id="value-props">
<div class="nb-section-header nb-reveal">
<div class="nb-section-label">Why choose this</div>
<h2>Unlock your <span class="nb-glow-primary">potential</span></h2>
<p>Everything you need to create without limits</p>
</div>
<div class="nb-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose this{{end}}</div>
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}Unlock your <span class="nb-glow-primary">potential</span>{{end}}</h2>
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
</div>
<div class="nb-value-grid">
{{range .Config.ValueProps}}
<div class="nb-value-tile nb-reveal">
@@ -1426,8 +1439,9 @@
<!-- Features Section -->
<section class="nb-features" id="features">
<div class="nb-section-header nb-reveal">
<div class="nb-section-label">Capabilities</div>
<h2>Packed with <span class="nb-glow-text">power</span></h2>
<div class="nb-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Packed with <span class="nb-glow-text">power</span>{{end}}</h2>
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
</div>
<div class="nb-feature-list">
{{range .Config.Features}}
@@ -1447,7 +1461,7 @@
<!-- Pricing Section -->
<section class="nb-pricing" id="pricing">
<div class="nb-section-header nb-reveal">
<div class="nb-section-label">Investment</div>
<div class="nb-section-label">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Investment{{end}}</div>
<h2>{{if .Config.Pricing.Headline}}{{.Config.Pricing.Headline}}{{else}}Simple <span class="nb-glow-text">Pricing</span>{{end}}</h2>
<p>Choose the plan that works for you</p>
</div>
@@ -1474,7 +1488,7 @@
<!-- Testimonials Section -->
{{if .Config.SocialProof.Testimonials}}
<section class="nb-testimonial-section">
<section class="nb-testimonial-section" id="testimonials">
<div class="nb-testimonial-block nb-reveal">
<span class="nb-quote-mark">&ldquo;</span>
<div class="nb-testimonials-container">
@@ -1507,8 +1521,34 @@
{{end}}
{{if .Config.CTASection.Headline}}
<!-- Cross-Promote Section -->
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
<section class="nb-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
<div class="nb-features-inner">
<div class="nb-section-header nb-reveal">
<div class="nb-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
</div>
<div class="nb-features-grid">
{{range .CrossPromoteItems}}
<a href="{{.URL}}" class="nb-feature-card nb-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
{{if .LogoURL}}
<div style="margin-bottom: 16px;">
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
</div>
{{end}}
<h3 class="nb-feature-title">{{.Name}}</h3>
{{if .Description}}<p class="nb-feature-desc">{{.Description}}</p>{{end}}
</a>
{{end}}
</div>
</div>
</section>
{{end}}
<!-- CTA Banner -->
<section class="nb-cta-section">
<section class="nb-cta-section" id="cta">
<div class="nb-cta-banner nb-reveal">
<h2>{{.Config.CTASection.Headline}}</h2>
{{if .Config.CTASection.Subheadline}}
@@ -1528,7 +1568,7 @@
{{if and .Config.Blog.Enabled .BlogPosts}}
<section class="nb-features" id="blog" style="border-top: 1px solid var(--nb-border-hard);">
<div class="nb-section-header nb-reveal">
<div class="nb-section-label">Blog</div>
<div class="nb-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}<span class="nb-glow-text">{{.Config.Blog.Headline}}</span>{{else}}Latest <span class="nb-glow-text">Posts</span>{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1566,7 +1606,7 @@
{{if and .Config.Gallery.Enabled .GalleryImages}}
<section class="nb-features" id="gallery" style="border-top: 1px solid var(--nb-border-hard);">
<div class="nb-section-header nb-reveal">
<div class="nb-section-label">Gallery</div>
<div class="nb-section-label">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</div>
<h2><span class="nb-glow-text">{{if .Config.Gallery.Headline}}{{.Config.Gallery.Headline}}{{else}}Gallery{{end}}</span></h2>
{{if .Config.Gallery.Subheadline}}<p>{{.Config.Gallery.Subheadline}}</p>{{end}}
</div>
@@ -1590,7 +1630,7 @@
<section class="nb-features" id="comparison" style="border-top: 1px solid var(--nb-border);">
<div class="nb-features-inner">
<div class="nb-section-header nb-reveal">
<div class="nb-section-label">Compare</div>
<div class="nb-section-label">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</div>
<h2>{{if .Config.Comparison.Headline}}{{.Config.Comparison.Headline}}{{else}}How We Compare{{end}}</h2>
{{if .Config.Comparison.Subheadline}}<p>{{.Config.Comparison.Subheadline}}</p>{{end}}
</div>
@@ -1676,9 +1716,9 @@
<a href="{{.URL}}" class="nb-footer-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowRepository}}<a href="{{.RepoURL}}" class="nb-footer-link">Repository</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="nb-footer-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="nb-footer-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="nb-footer-link">Issues</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="nb-footer-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="nb-footer-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="nb-footer-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
</div>
</footer>
</div>

View File

@@ -970,7 +970,19 @@
/* Responsive */
@media (max-width: 768px) {
.ct-nav { padding: 0 20px; }
.ct-nav-links { display: none; }
.ct-nav-links {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
padding: 16px 24px;
gap: 8px;
background: inherit;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 100;
}
.ct-menu-toggle { display: flex; }
.ct-hero { padding: 120px 20px 80px; }
.ct-hero-ctas { flex-direction: column; align-items: center; }
@@ -1012,16 +1024,17 @@
{{range .Config.Footer.Links}}
<a href="{{.URL}}" class="ct-nav-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="ct-nav-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="ct-nav-link">API</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ct-nav-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ct-nav-link">Issues</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="ct-nav-link">Value Props</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="ct-nav-link">Features</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="ct-nav-link">Pricing</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="ct-nav-link">Blog</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="ct-nav-link">Gallery</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="ct-nav-link">Compare</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="ct-nav-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="ct-nav-link">{{if .Config.Navigation.LabelAPI}}{{.Config.Navigation.LabelAPI}}{{else}}API{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ct-nav-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ct-nav-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="ct-nav-link">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Value Props{{end}}</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="ct-nav-link">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="ct-nav-link">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="ct-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="ct-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="ct-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="ct-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
{{if .Config.Navigation.ShowRepository}}
<a href="{{.RepoURL}}" class="ct-nav-cta">
<img src="/assets/img/gitcaddy-icon.svg" width="14" height="14" alt="GitCaddy">
@@ -1077,7 +1090,7 @@
<section class="ct-features" style="padding-top: 120px;">
<div class="ct-features-inner">
<div class="ct-section-header ct-reveal">
<div class="ct-section-label">Blog</div>
<div class="ct-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}All Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1108,7 +1121,7 @@
</section>
{{else}}
<!-- Hero Section -->
<section class="ct-hero">
<section class="ct-hero" id="hero">
<div class="ct-hero-glow"></div>
<div class="ct-hero-vignette"></div>
@@ -1130,11 +1143,6 @@
{{.Config.Hero.PrimaryCTA.Label}}
{{svg "octicon-arrow-right" 16}}
</a>
{{else}}
<a href="{{.RepoURL}}" class="ct-btn-primary" data-cta="primary">
Get Started
{{svg "octicon-arrow-right" 16}}
</a>
{{end}}
{{if .Config.Hero.SecondaryCTA.Label}}
@@ -1142,14 +1150,26 @@
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
{{.Config.Hero.SecondaryCTA.Label}}
</a>
{{else}}
<a href="{{.RepoURL}}" class="ct-btn-secondary" data-cta="secondary">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
View Source
</a>
{{end}}
</div>
{{if or $.GooglePlayID $.AppStoreID}}
<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 16px;" class="ct-reveal visible ct-reveal-delay-4">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="ct-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="ct-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
{{end}}
{{if .Config.Hero.CodeExample}}
<div class="ct-code-block ct-reveal visible ct-reveal-delay-4">
<div class="ct-code-titlebar">
@@ -1172,7 +1192,7 @@
<!-- Stats Section -->
{{if or .Config.Stats (gt .NumStars 0)}}
<section class="ct-stats">
<section class="ct-stats" id="stats">
<div class="ct-stats-inner ct-reveal">
{{if .Config.Stats}}
{{range .Config.Stats}}
@@ -1203,7 +1223,7 @@
<!-- Downloads Section -->
{{if and .PublicReleases .LatestRelease .LatestRelease.Attachments}}
<section class="ct-downloads">
<section class="ct-downloads" id="downloads">
<div class="ct-downloads-inner ct-reveal">
<h2>Download v{{.LatestReleaseTag}}</h2>
<p>Get the latest release</p>
@@ -1266,25 +1286,6 @@
</div>
{{end}}
{{end}}
{{if or $.GooglePlayID $.AppStoreID}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 12px; color: var(--ct-muted); letter-spacing: 0.08em; text-transform: uppercase;">{{svg "octicon-device-mobile" 16}} App Stores</h4>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="ct-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="ct-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
</div>
{{end}}
{{if $otherFiles}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 12px; color: var(--ct-muted); letter-spacing: 0.08em; text-transform: uppercase;">{{svg "octicon-file" 16}} Other</h4>
@@ -1302,10 +1303,10 @@
<section class="ct-features" id="value-props">
<div class="ct-features-inner">
<div class="ct-section-header ct-reveal">
<div class="ct-section-label">Why choose us</div>
<h2>{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}</h2>
<p>Everything you need to get started quickly.</p>
</div>
<div class="ct-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose us{{end}}</div>
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
</div>
<div class="ct-features-grid">
{{range .Config.ValueProps}}
<div class="ct-feature-card ct-reveal">
@@ -1326,10 +1327,10 @@
<section class="ct-features" id="features" style="padding-top: 40px;">
<div class="ct-features-inner">
<div class="ct-section-header ct-reveal">
<div class="ct-section-label">Capabilities</div>
<h2>Features</h2>
<p>Powerful capabilities at your fingertips.</p>
</div>
<div class="ct-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
</div>
<div class="ct-features-grid">
{{range .Config.Features}}
<div class="ct-feature-card ct-reveal">
@@ -1347,7 +1348,7 @@
<!-- Social Proof -->
{{if or .Config.SocialProof.Logos .Config.SocialProof.Testimonials}}
<section class="ct-social-proof">
<section class="ct-social-proof" id="social-proof">
<div class="ct-social-proof-inner">
{{if .Config.SocialProof.Logos}}
<div class="ct-logos ct-reveal">
@@ -1387,7 +1388,7 @@
<section class="ct-pricing" id="pricing">
<div class="ct-pricing-inner">
<div class="ct-section-header ct-reveal">
<div class="ct-section-label">Pricing</div>
<div class="ct-section-label">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</div>
<h2>{{if .Config.Pricing.Headline}}{{.Config.Pricing.Headline}}{{else}}Pricing{{end}}</h2>
<p>{{if .Config.Pricing.Subheadline}}{{.Config.Pricing.Subheadline}}{{else}}Choose the plan that works for you{{end}}</p>
</div>
@@ -1411,9 +1412,35 @@
</section>
{{end}}
<!-- Cross-Promote Section -->
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
<section class="ct-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
<div class="ct-features-inner">
<div class="ct-section-header ct-reveal">
<div class="ct-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
</div>
<div class="ct-features-grid">
{{range .CrossPromoteItems}}
<a href="{{.URL}}" class="ct-feature-card ct-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
{{if .LogoURL}}
<div style="margin-bottom: 16px;">
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
</div>
{{end}}
<h3 class="ct-feature-title">{{.Name}}</h3>
{{if .Description}}<p class="ct-feature-desc">{{.Description}}</p>{{end}}
</a>
{{end}}
</div>
</div>
</section>
{{end}}
<!-- CTA Section -->
{{if .Config.CTASection.Headline}}
<section class="ct-cta-section">
<section class="ct-cta-section" id="cta">
<div class="ct-cta-inner ct-reveal">
<h2>{{.Config.CTASection.Headline}}</h2>
{{if .Config.CTASection.Subheadline}}
@@ -1432,7 +1459,7 @@
<section class="ct-features" id="blog" style="border-top: 1px solid var(--ct-dim);">
<div class="ct-features-inner">
<div class="ct-section-header ct-reveal">
<div class="ct-section-label">Blog</div>
<div class="ct-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}Latest Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1471,7 +1498,7 @@
<section class="ct-features" id="gallery" style="border-top: 1px solid var(--ct-dim);">
<div class="ct-features-inner">
<div class="ct-section-header ct-reveal">
<div class="ct-section-label">Gallery</div>
<div class="ct-section-label">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</div>
<h2>{{if .Config.Gallery.Headline}}{{.Config.Gallery.Headline}}{{else}}Gallery{{end}}</h2>
{{if .Config.Gallery.Subheadline}}<p>{{.Config.Gallery.Subheadline}}</p>{{end}}
</div>
@@ -1496,7 +1523,7 @@
<section class="ct-features" id="comparison" style="border-top: 1px solid var(--ct-dim);">
<div class="ct-features-inner">
<div class="ct-section-header ct-reveal">
<div class="ct-section-label">Compare</div>
<div class="ct-section-label">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</div>
<h2>{{if .Config.Comparison.Headline}}{{.Config.Comparison.Headline}}{{else}}How We Compare{{end}}</h2>
{{if .Config.Comparison.Subheadline}}<p>{{.Config.Comparison.Subheadline}}</p>{{end}}
</div>
@@ -1579,8 +1606,8 @@
{{end}}
{{if .Config.Navigation.ShowRepository}}<a href="{{.RepoURL}}" class="ct-footer-link">Repository</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="ct-footer-link">Documentation</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ct-footer-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ct-footer-link">Issues</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ct-footer-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ct-footer-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
</div>
</footer>
</div>

View File

@@ -855,7 +855,19 @@
/* Responsive */
@media (max-width: 768px) {
.dt-nav { padding: 0 20px; }
.dt-nav-links { display: none; }
.dt-nav-links {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
padding: 16px 24px;
gap: 8px;
background: inherit;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 100;
}
.dt-menu-toggle { display: flex; }
.dt-hero { padding: 100px 24px 80px; }
.dt-hero-ctas { flex-direction: column; align-items: center; }
@@ -895,16 +907,17 @@
{{range .Config.Footer.Links}}
<a href="{{.URL}}" class="dt-nav-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="dt-nav-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="dt-nav-link">API</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="dt-nav-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="dt-nav-link">Issues</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="dt-nav-link">Value Props</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="dt-nav-link">Features</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="dt-nav-link">Pricing</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="dt-nav-link">Blog</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="dt-nav-link">Gallery</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="dt-nav-link">Compare</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="dt-nav-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="dt-nav-link">{{if .Config.Navigation.LabelAPI}}{{.Config.Navigation.LabelAPI}}{{else}}API{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="dt-nav-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="dt-nav-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="dt-nav-link">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Value Props{{end}}</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="dt-nav-link">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="dt-nav-link">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="dt-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="dt-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="dt-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="dt-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
{{if .Config.Navigation.ShowRepository}}
<a href="{{.RepoURL}}" class="dt-nav-cta">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
@@ -960,7 +973,7 @@
<section class="dt-features" style="padding-top: 100px;">
<div class="dt-features-inner">
<div class="dt-section-header dt-reveal">
<div class="dt-section-label">Blog</div>
<div class="dt-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}All Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -991,7 +1004,7 @@
</section>
{{else}}
<!-- Hero Section -->
<section class="dt-hero">
<section class="dt-hero" id="hero">
<div class="dt-hero-content">
<div class="dt-badge dt-reveal visible">
<span class="dt-badge-label">status</span>
@@ -1011,11 +1024,6 @@
{{.Config.Hero.PrimaryCTA.Label}}
{{svg "octicon-arrow-right" 16}}
</a>
{{else}}
<a href="{{.RepoURL}}" class="dt-btn-primary" data-cta="primary">
Get Started
{{svg "octicon-arrow-right" 16}}
</a>
{{end}}
{{if .Config.Hero.SecondaryCTA.Label}}
@@ -1023,14 +1031,26 @@
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
{{.Config.Hero.SecondaryCTA.Label}}
</a>
{{else}}
<a href="{{.RepoURL}}" class="dt-btn-secondary" data-cta="secondary">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
View Source
</a>
{{end}}
</div>
{{if or $.GooglePlayID $.AppStoreID}}
<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 16px;" class="dt-reveal visible dt-reveal-delay-4">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="dt-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="dt-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
{{end}}
{{if .Config.Hero.CodeExample}}
<div class="dt-code-block dt-reveal visible dt-reveal-delay-4">
<span class="dt-code-prompt">$</span>
@@ -1045,7 +1065,7 @@
<!-- Stats Section -->
{{if or .Config.Stats (gt .NumStars 0)}}
<section class="dt-stats">
<section class="dt-stats" id="stats">
<div class="dt-stats-inner dt-reveal">
{{if .Config.Stats}}
{{range .Config.Stats}}
@@ -1076,7 +1096,7 @@
<!-- Downloads Section -->
{{if and .PublicReleases .LatestRelease .LatestRelease.Attachments}}
<section class="dt-downloads">
<section class="dt-downloads" id="downloads">
<div class="dt-downloads-inner dt-reveal">
<h2>Download v{{.LatestReleaseTag}}</h2>
<p>Get the latest release</p>
@@ -1139,25 +1159,6 @@
</div>
{{end}}
{{end}}
{{if or $.GooglePlayID $.AppStoreID}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 12px; color: var(--dt-muted); font-family: 'JetBrains Mono', monospace; letter-spacing: 0.08em; text-transform: uppercase;">{{svg "octicon-device-mobile" 16}} App Stores</h4>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="dt-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="dt-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
</div>
{{end}}
{{if $otherFiles}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 12px; color: var(--dt-muted); font-family: 'JetBrains Mono', monospace; letter-spacing: 0.08em; text-transform: uppercase;">{{svg "octicon-file" 16}} Other</h4>
@@ -1175,10 +1176,10 @@
<section class="dt-features" id="value-props">
<div class="dt-features-inner">
<div class="dt-section-header dt-reveal">
<div class="dt-section-label">Why choose us</div>
<h2>{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}</h2>
<p>Everything you need to get started quickly.</p>
</div>
<div class="dt-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose us{{end}}</div>
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
</div>
<div class="dt-features-grid">
{{range .Config.ValueProps}}
<div class="dt-feature-card dt-reveal">
@@ -1199,10 +1200,10 @@
<section class="dt-features" id="features" style="padding-top: 40px;">
<div class="dt-features-inner">
<div class="dt-section-header dt-reveal">
<div class="dt-section-label">Capabilities</div>
<h2>Features</h2>
<p>Powerful capabilities at your fingertips.</p>
</div>
<div class="dt-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
</div>
<div class="dt-features-grid">
{{range .Config.Features}}
<div class="dt-feature-card dt-reveal">
@@ -1220,7 +1221,7 @@
<!-- Social Proof -->
{{if or .Config.SocialProof.Logos .Config.SocialProof.Testimonials}}
<section class="dt-social-proof">
<section class="dt-social-proof" id="social-proof">
<div class="dt-social-proof-inner">
{{if .Config.SocialProof.Logos}}
<div class="dt-logos dt-reveal">
@@ -1260,7 +1261,7 @@
<section class="dt-pricing" id="pricing">
<div class="dt-pricing-inner">
<div class="dt-section-header dt-reveal">
<div class="dt-section-label">Pricing</div>
<div class="dt-section-label">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</div>
<h2>{{if .Config.Pricing.Headline}}{{.Config.Pricing.Headline}}{{else}}Pricing{{end}}</h2>
<p>{{if .Config.Pricing.Subheadline}}{{.Config.Pricing.Subheadline}}{{else}}Choose the plan that works for you{{end}}</p>
</div>
@@ -1284,9 +1285,35 @@
</section>
{{end}}
<!-- Cross-Promote Section -->
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
<section class="dt-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
<div class="dt-features-inner">
<div class="dt-section-header dt-reveal">
<div class="dt-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
</div>
<div class="dt-features-grid">
{{range .CrossPromoteItems}}
<a href="{{.URL}}" class="dt-feature-card dt-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
{{if .LogoURL}}
<div style="margin-bottom: 16px;">
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
</div>
{{end}}
<h3 class="dt-feature-title">{{.Name}}</h3>
{{if .Description}}<p class="dt-feature-desc">{{.Description}}</p>{{end}}
</a>
{{end}}
</div>
</div>
</section>
{{end}}
<!-- CTA Section -->
{{if .Config.CTASection.Headline}}
<section class="dt-cta-section">
<section class="dt-cta-section" id="cta">
<div class="dt-cta-inner dt-reveal">
<h2>{{.Config.CTASection.Headline}}</h2>
{{if .Config.CTASection.Subheadline}}
@@ -1305,7 +1332,7 @@
<section class="dt-features" id="blog" style="border-top: 1px solid var(--dt-dim);">
<div class="dt-features-inner">
<div class="dt-section-header dt-reveal">
<div class="dt-section-label">Blog</div>
<div class="dt-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}Latest Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1344,7 +1371,7 @@
<section class="dt-features" id="gallery" style="border-top: 1px solid var(--dt-dim);">
<div class="dt-features-inner">
<div class="dt-section-header dt-reveal">
<div class="dt-section-label">Gallery</div>
<div class="dt-section-label">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</div>
<h2>{{if .Config.Gallery.Headline}}{{.Config.Gallery.Headline}}{{else}}Gallery{{end}}</h2>
{{if .Config.Gallery.Subheadline}}<p>{{.Config.Gallery.Subheadline}}</p>{{end}}
</div>
@@ -1369,7 +1396,7 @@
<section class="dt-features" id="comparison" style="border-top: 1px solid var(--dt-dim);">
<div class="dt-features-inner">
<div class="dt-section-header dt-reveal">
<div class="dt-section-label">Compare</div>
<div class="dt-section-label">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</div>
<h2>{{if .Config.Comparison.Headline}}{{.Config.Comparison.Headline}}{{else}}How We Compare{{end}}</h2>
{{if .Config.Comparison.Subheadline}}<p>{{.Config.Comparison.Subheadline}}</p>{{end}}
</div>
@@ -1452,8 +1479,8 @@
{{end}}
{{if .Config.Navigation.ShowRepository}}<a href="{{.RepoURL}}" class="dt-footer-link">Repository</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="dt-footer-link">Documentation</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="dt-footer-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="dt-footer-link">Issues</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="dt-footer-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="dt-footer-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
</div>
</footer>
</div>

View File

@@ -880,7 +880,19 @@
/* Responsive */
@media (max-width: 768px) {
.df-nav { padding: 0 20px; }
.df-nav-links { display: none; }
.df-nav-links {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
padding: 16px 24px;
gap: 8px;
background: inherit;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 100;
}
.df-menu-toggle { display: flex; }
.df-hero { padding: 120px 24px 80px; }
.df-hero-ctas { flex-direction: column; align-items: center; }
@@ -920,16 +932,17 @@
{{range .Config.Footer.Links}}
<a href="{{.URL}}" class="df-nav-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="df-nav-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="df-nav-link">API</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="df-nav-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="df-nav-link">Issues</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="df-nav-link">Value Props</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="df-nav-link">Features</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="df-nav-link">Pricing</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="df-nav-link">Blog</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="df-nav-link">Gallery</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="df-nav-link">Compare</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="df-nav-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="df-nav-link">{{if .Config.Navigation.LabelAPI}}{{.Config.Navigation.LabelAPI}}{{else}}API{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="df-nav-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="df-nav-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="df-nav-link">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Value Props{{end}}</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="df-nav-link">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="df-nav-link">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="df-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="df-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="df-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="df-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
{{if .Config.Navigation.ShowRepository}}
<a href="{{.RepoURL}}" class="df-nav-cta">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
@@ -985,7 +998,7 @@
<section class="df-features" style="padding-top: 120px;">
<div class="df-features-inner">
<div class="df-section-header df-reveal">
<div class="df-section-label">Blog</div>
<div class="df-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}All Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1016,7 +1029,7 @@
</section>
{{else}}
<!-- Hero Section -->
<section class="df-hero">
<section class="df-hero" id="hero">
<div class="df-hero-content">
<div class="df-badge df-reveal visible">
<span class="df-badge-dot"></span>
@@ -1035,11 +1048,6 @@
{{.Config.Hero.PrimaryCTA.Label}}
{{svg "octicon-arrow-right" 16}}
</a>
{{else}}
<a href="{{.RepoURL}}" class="df-btn-primary" data-cta="primary">
Get Started
{{svg "octicon-arrow-right" 16}}
</a>
{{end}}
{{if .Config.Hero.SecondaryCTA.Label}}
@@ -1047,14 +1055,26 @@
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
{{.Config.Hero.SecondaryCTA.Label}}
</a>
{{else}}
<a href="{{.RepoURL}}" class="df-btn-secondary" data-cta="secondary">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
View Source
</a>
{{end}}
</div>
{{if or $.GooglePlayID $.AppStoreID}}
<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 16px;" class="df-reveal visible df-reveal-delay-4">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="df-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="df-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
{{end}}
{{if .Config.Hero.CodeExample}}
<div class="df-code-block df-reveal visible df-reveal-delay-4">
<span class="df-code-prompt">$</span>
@@ -1069,7 +1089,7 @@
<!-- Stats Section -->
{{if or .Config.Stats (gt .NumStars 0)}}
<section class="df-stats">
<section class="df-stats" id="stats">
<div class="df-stats-inner df-reveal">
{{if .Config.Stats}}
{{range .Config.Stats}}
@@ -1100,7 +1120,7 @@
<!-- Downloads Section -->
{{if and .PublicReleases .LatestRelease .LatestRelease.Attachments}}
<section class="df-downloads">
<section class="df-downloads" id="downloads">
<div class="df-downloads-inner df-reveal">
<h2>Download v{{.LatestReleaseTag}}</h2>
<p>Get the latest release</p>
@@ -1163,25 +1183,6 @@
</div>
{{end}}
{{end}}
{{if or $.GooglePlayID $.AppStoreID}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 13px; color: var(--df-muted); letter-spacing: 0.08em; text-transform: uppercase; font-weight: 700;">{{svg "octicon-device-mobile" 16}} App Stores</h4>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="df-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="df-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
</div>
{{end}}
{{if $otherFiles}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 13px; color: var(--df-muted); letter-spacing: 0.08em; text-transform: uppercase; font-weight: 700;">{{svg "octicon-file" 16}} Other</h4>
@@ -1199,10 +1200,10 @@
<section class="df-features" id="value-props">
<div class="df-features-inner">
<div class="df-section-header df-reveal">
<div class="df-section-label">Why choose us</div>
<h2>{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}</h2>
<p>Everything you need to get started quickly.</p>
</div>
<div class="df-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose us{{end}}</div>
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
</div>
<div class="df-features-grid">
{{range .Config.ValueProps}}
<div class="df-feature-card df-reveal">
@@ -1223,10 +1224,10 @@
<section class="df-features" id="features" style="padding-top: 40px;">
<div class="df-features-inner">
<div class="df-section-header df-reveal">
<div class="df-section-label">Capabilities</div>
<h2>Features</h2>
<p>Powerful capabilities at your fingertips.</p>
</div>
<div class="df-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
</div>
<div class="df-features-grid">
{{range .Config.Features}}
<div class="df-feature-card df-reveal">
@@ -1244,7 +1245,7 @@
<!-- Social Proof -->
{{if or .Config.SocialProof.Logos .Config.SocialProof.Testimonials}}
<section class="df-social-proof">
<section class="df-social-proof" id="social-proof">
<div class="df-social-proof-inner">
{{if .Config.SocialProof.Logos}}
<div class="df-logos df-reveal">
@@ -1284,7 +1285,7 @@
<section class="df-pricing" id="pricing">
<div class="df-pricing-inner">
<div class="df-section-header df-reveal">
<div class="df-section-label">Pricing</div>
<div class="df-section-label">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</div>
<h2>{{if .Config.Pricing.Headline}}{{.Config.Pricing.Headline}}{{else}}Pricing{{end}}</h2>
<p>{{if .Config.Pricing.Subheadline}}{{.Config.Pricing.Subheadline}}{{else}}Choose the plan that works for you{{end}}</p>
</div>
@@ -1308,9 +1309,35 @@
</section>
{{end}}
<!-- Cross-Promote Section -->
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
<section class="df-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
<div class="df-features-inner">
<div class="df-section-header df-reveal">
<div class="df-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
</div>
<div class="df-features-grid">
{{range .CrossPromoteItems}}
<a href="{{.URL}}" class="df-feature-card df-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
{{if .LogoURL}}
<div style="margin-bottom: 16px;">
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
</div>
{{end}}
<h3 class="df-feature-title">{{.Name}}</h3>
{{if .Description}}<p class="df-feature-desc">{{.Description}}</p>{{end}}
</a>
{{end}}
</div>
</div>
</section>
{{end}}
<!-- CTA Section -->
{{if .Config.CTASection.Headline}}
<section class="df-cta-section">
<section class="df-cta-section" id="cta">
<div class="df-cta-inner df-reveal">
<h2>{{.Config.CTASection.Headline}}</h2>
{{if .Config.CTASection.Subheadline}}
@@ -1329,7 +1356,7 @@
<section class="df-features" id="blog" style="border-top: 1px solid var(--df-border);">
<div class="df-features-inner">
<div class="df-section-header df-reveal">
<div class="df-section-label">Blog</div>
<div class="df-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}Latest Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1368,7 +1395,7 @@
<section class="df-features" id="gallery" style="border-top: 1px solid var(--df-border);">
<div class="df-features-inner">
<div class="df-section-header df-reveal">
<div class="df-section-label">Gallery</div>
<div class="df-section-label">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</div>
<h2>{{if .Config.Gallery.Headline}}{{.Config.Gallery.Headline}}{{else}}Gallery{{end}}</h2>
{{if .Config.Gallery.Subheadline}}<p>{{.Config.Gallery.Subheadline}}</p>{{end}}
</div>
@@ -1393,7 +1420,7 @@
<section class="df-features" id="comparison" style="border-top: 1px solid var(--df-border);">
<div class="df-features-inner">
<div class="df-section-header df-reveal">
<div class="df-section-label">Compare</div>
<div class="df-section-label">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</div>
<h2>{{if .Config.Comparison.Headline}}{{.Config.Comparison.Headline}}{{else}}How We Compare{{end}}</h2>
{{if .Config.Comparison.Subheadline}}<p>{{.Config.Comparison.Subheadline}}</p>{{end}}
</div>
@@ -1476,8 +1503,8 @@
{{end}}
{{if .Config.Navigation.ShowRepository}}<a href="{{.RepoURL}}" class="df-footer-link">Repository</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="df-footer-link">Documentation</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="df-footer-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="df-footer-link">Issues</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="df-footer-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="df-footer-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
</div>
</footer>
</div>

View File

@@ -11,9 +11,9 @@
<div class="pages-footer-column">
<ul class="pages-footer-list">
{{if .Config.Navigation.ShowRepository}}<li><a href="{{AppSubUrl}}/{{.Repository.FullName}}">Repository</a></li>{{end}}
{{if .Config.Navigation.ShowDocs}}<li><a href="{{AppSubUrl}}/{{.Repository.FullName}}/wiki">Documentation</a></li>{{end}}
{{if .Config.Navigation.ShowReleases}}<li><a href="{{AppSubUrl}}/{{.Repository.FullName}}/releases">Releases</a></li>{{end}}
{{if .Config.Navigation.ShowIssues}}<li><a href="{{AppSubUrl}}/{{.Repository.FullName}}/issues">Issues</a></li>{{end}}
{{if .Config.Navigation.ShowDocs}}<li><a href="{{AppSubUrl}}/{{.Repository.FullName}}/wiki">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Documentation{{end}}</a></li>{{end}}
{{if .Config.Navigation.ShowReleases}}<li><a href="{{AppSubUrl}}/{{.Repository.FullName}}/releases">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a></li>{{end}}
{{if .Config.Navigation.ShowIssues}}<li><a href="{{AppSubUrl}}/{{.Repository.FullName}}/issues">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a></li>{{end}}
</ul>
</div>
</div>

View File

@@ -12,10 +12,10 @@
{{range .Config.Footer.Links}}
<a href="{{.URL}}" class="pages-nav-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{AppSubUrl}}/{{.Repository.FullName}}/wiki" class="pages-nav-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{AppSubUrl}}/{{.Repository.FullName}}/swagger" class="pages-nav-link">API</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{AppSubUrl}}/{{.Repository.FullName}}/releases" class="pages-nav-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{AppSubUrl}}/{{.Repository.FullName}}/issues" class="pages-nav-link">Issues</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{AppSubUrl}}/{{.Repository.FullName}}/wiki" class="pages-nav-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{AppSubUrl}}/{{.Repository.FullName}}/swagger" class="pages-nav-link">{{if .Config.Navigation.LabelAPI}}{{.Config.Navigation.LabelAPI}}{{else}}API{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{AppSubUrl}}/{{.Repository.FullName}}/releases" class="pages-nav-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{AppSubUrl}}/{{.Repository.FullName}}/issues" class="pages-nav-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
{{if .Config.Navigation.ShowRepository}}
<a href="{{AppSubUrl}}/{{.Repository.FullName}}" class="ui mini button" target="_blank">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy"> View Source

View File

@@ -995,16 +995,17 @@
{{range .Config.Footer.Links}}
<a href="{{.URL}}" class="ea-nav-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="ea-nav-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="ea-nav-link">API</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ea-nav-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ea-nav-link">Issues</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#why" class="ea-nav-link">Why</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="ea-nav-link">Features</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="ea-nav-link">Pricing</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="ea-nav-link">Blog</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="ea-nav-link">Gallery</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="ea-nav-link">Compare</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="ea-nav-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="ea-nav-link">{{if .Config.Navigation.LabelAPI}}{{.Config.Navigation.LabelAPI}}{{else}}API{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ea-nav-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ea-nav-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#why" class="ea-nav-link">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why{{end}}</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="ea-nav-link">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="ea-nav-link">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="ea-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="ea-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="ea-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="ea-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
{{if .Config.Navigation.ShowRepository}}
<a href="{{.RepoURL}}" class="ea-btn-text">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
@@ -1030,16 +1031,17 @@
{{range .Config.Footer.Links}}
<a href="{{.URL}}" class="ea-nav-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="ea-nav-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="ea-nav-link">API</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ea-nav-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ea-nav-link">Issues</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#why" class="ea-nav-link">Why</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="ea-nav-link">Features</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="ea-nav-link">Pricing</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="ea-nav-link">Blog</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="ea-nav-link">Gallery</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="ea-nav-link">Compare</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="ea-nav-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="ea-nav-link">{{if .Config.Navigation.LabelAPI}}{{.Config.Navigation.LabelAPI}}{{else}}API{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ea-nav-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ea-nav-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#why" class="ea-nav-link">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why{{end}}</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="ea-nav-link">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="ea-nav-link">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="ea-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="ea-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="ea-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="ea-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
{{if .Config.Navigation.ShowRepository}}
<a href="{{.RepoURL}}" class="ea-btn-text">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
@@ -1079,7 +1081,7 @@
<section class="ea-section" style="padding-top: 100px;">
<div class="ea-section-inner">
<div class="ea-section-header ea-reveal">
<div class="ea-section-eyebrow">Blog</div>
<div class="ea-section-eyebrow">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}All Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1112,7 +1114,7 @@
</section>
{{else}}
<!-- Hero Section -->
<section class="ea-hero">
<section class="ea-hero" id="hero">
<div class="ea-reveal">
<div class="ea-hero-eyebrow">{{if .Config.Brand.Name}}{{.Config.Brand.Name}}{{else}}Open Source{{end}}</div>
</div>
@@ -1125,14 +1127,26 @@
{{.Config.Hero.PrimaryCTA.Label}}
{{svg "octicon-arrow-right" 14}}
</a>
{{else}}
<a href="{{.RepoURL}}" class="ea-btn-primary" data-cta="primary">
Get Started
{{svg "octicon-arrow-right" 14}}
</a>
{{end}}
</div>
{{if or $.GooglePlayID $.AppStoreID}}
<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 16px;" class="ea-reveal ea-reveal-delay-4">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="ea-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="ea-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
{{end}}
{{if .Config.Hero.CodeExample}}
<div class="ea-code-block ea-reveal ea-reveal-delay-4">
<button class="ea-copy-btn" onclick="navigator.clipboard.writeText(document.getElementById('code-example').textContent)">
@@ -1146,7 +1160,7 @@
{{if or .Config.Stats (gt .NumStars 0)}}
<div class="ea-rule"><div class="ea-rule-inner"></div></div>
<!-- Stats -->
<section class="ea-stats">
<section class="ea-stats" id="stats">
<div class="ea-stats-inner">
{{if .Config.Stats}}
{{range .Config.Stats}}
@@ -1177,7 +1191,7 @@
{{end}}
{{if and .PublicReleases .LatestRelease .LatestRelease.Attachments}}
<section class="ea-downloads">
<section class="ea-downloads" id="downloads">
<div class="ea-section-label ea-reveal">Downloads</div>
<h2 class="ea-section-title ea-reveal">v{{.LatestReleaseTag}}</h2>
<p style="color: var(--ea-muted); font-size: 17px; margin-bottom: 28px;" class="ea-reveal">Get the latest release</p>
@@ -1240,25 +1254,6 @@
</div>
{{end}}
{{end}}
{{if or $.GooglePlayID $.AppStoreID}}
<div style="margin-bottom: 20px;" class="ea-reveal">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--ea-light); letter-spacing: 0.08em; text-transform: uppercase;">{{svg "octicon-device-mobile" 14}} App Stores</h4>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="ea-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="ea-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
</div>
{{end}}
{{if $otherFiles}}
<div style="margin-bottom: 20px;" class="ea-reveal">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--ea-light); letter-spacing: 0.08em; text-transform: uppercase;">{{svg "octicon-file" 14}} Other</h4>
@@ -1274,8 +1269,9 @@
<!-- Value Props -->
{{if .Config.ValueProps}}
<section class="ea-section" id="why">
<div class="ea-section-label ea-reveal">Why choose this</div>
<h2 class="ea-section-title ea-reveal">{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}{{else}}Why Choose Us{{end}}</h2>
<div class="ea-section-label ea-reveal">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose this{{end}}</div>
<h2 class="ea-section-title ea-reveal">{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}{{else}}Why Choose Us{{end}}{{end}}</h2>
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
{{range $i, $v := .Config.ValueProps}}
<div class="ea-value-item ea-reveal">
<div class="ea-value-number">{{$i | printf "%d"}}</div>
@@ -1291,8 +1287,9 @@
<!-- Features Accordion -->
{{if .Config.Features}}
<section class="ea-section" id="features">
<div class="ea-section-label ea-reveal">Capabilities</div>
<h2 class="ea-section-title ea-reveal">Features</h2>
<div class="ea-section-label ea-reveal">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
<h2 class="ea-section-title ea-reveal">{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
<div class="ea-accordion ea-reveal">
{{range $i, $f := .Config.Features}}
<div class="ea-accordion-item" data-index="{{$i}}">
@@ -1312,7 +1309,7 @@
<!-- Pricing Section -->
{{if .Config.Pricing.Plans}}
<section class="ea-pricing" id="pricing">
<div class="ea-section-label ea-reveal" style="max-width: 780px; margin: 0 auto 20px;">Investment</div>
<div class="ea-section-label ea-reveal" style="max-width: 780px; margin: 0 auto 20px;">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Investment{{end}}</div>
<h2 class="ea-pricing-title ea-reveal">{{if .Config.Pricing.Headline}}{{.Config.Pricing.Headline}}{{else}}Pricing{{end}}</h2>
<div class="ea-pricing-grid ea-reveal">
{{range .Config.Pricing.Plans}}
@@ -1337,7 +1334,7 @@
<!-- Testimonial -->
{{if .Config.SocialProof.Testimonials}}
<section class="ea-testimonial-section">
<section class="ea-testimonial-section" id="testimonials">
<div class="ea-testimonial-inner">
<div class="ea-testimonials-container">
{{range .Config.SocialProof.Testimonials}}
@@ -1369,7 +1366,7 @@
<!-- Used By -->
{{if .Config.SocialProof.Logos}}
<section class="ea-used-by">
<section class="ea-used-by" id="social-proof">
<div class="ea-section-label ea-reveal">Trusted by</div>
<div class="ea-used-by-logos ea-reveal ea-reveal-delay-1">
{{range .Config.SocialProof.Logos}}
@@ -1379,9 +1376,35 @@
</section>
{{end}}
<!-- Cross-Promote Section -->
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
<section class="ea-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
<div class="ea-features-inner">
<div class="ea-section-header ea-reveal">
<div class="ea-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
</div>
<div class="ea-features-grid">
{{range .CrossPromoteItems}}
<a href="{{.URL}}" class="ea-feature-card ea-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
{{if .LogoURL}}
<div style="margin-bottom: 16px;">
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
</div>
{{end}}
<h3 class="ea-feature-title">{{.Name}}</h3>
{{if .Description}}<p class="ea-feature-desc">{{.Description}}</p>{{end}}
</a>
{{end}}
</div>
</div>
</section>
{{end}}
<!-- CTA Section -->
{{if .Config.CTASection.Headline}}
<section class="ea-cta-section">
<section class="ea-cta-section" id="cta">
<h2 class="ea-reveal">{{.Config.CTASection.Headline}}</h2>
{{if .Config.CTASection.Subheadline}}
<button class="ea-install-cmd ea-reveal ea-reveal-delay-1" onclick="navigator.clipboard.writeText('{{.Config.CTASection.Subheadline}}')">
@@ -1404,7 +1427,7 @@
<!-- Blog Section -->
{{if and .Config.Blog.Enabled .BlogPosts}}
<section class="ea-section" id="blog">
<div class="ea-section-label ea-reveal">Blog</div>
<div class="ea-section-label ea-reveal">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2 class="ea-section-title ea-reveal">{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}Latest Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p style="color: var(--ea-muted); font-size: 17px; margin-bottom: 32px;" class="ea-reveal">{{.Config.Blog.Subheadline}}</p>{{end}}
{{range .BlogPosts}}
@@ -1439,7 +1462,7 @@
<!-- Gallery Section -->
{{if and .Config.Gallery.Enabled .GalleryImages}}
<section class="ea-section" id="gallery">
<div class="ea-section-label ea-reveal">Gallery</div>
<div class="ea-section-label ea-reveal">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</div>
<h2 class="ea-section-title ea-reveal">{{if .Config.Gallery.Headline}}{{.Config.Gallery.Headline}}{{else}}Gallery{{end}}</h2>
{{if .Config.Gallery.Subheadline}}<p style="color: var(--ea-muted); font-size: 17px; margin-bottom: 32px;" class="ea-reveal">{{.Config.Gallery.Subheadline}}</p>{{end}}
<div style="display: grid; grid-template-columns: repeat({{if .Config.Gallery.Columns}}{{.Config.Gallery.Columns}}{{else}}3{{end}}, 1fr); gap: 16px;">
@@ -1462,7 +1485,7 @@
<section class="ea-section" id="comparison" style="border-top: 1px solid var(--ea-border);">
<div class="ea-section-inner">
<div class="ea-section-header ea-reveal">
<div class="ea-section-label">Compare</div>
<div class="ea-section-label">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</div>
<h2>{{if .Config.Comparison.Headline}}{{.Config.Comparison.Headline}}{{else}}How We Compare{{end}}</h2>
{{if .Config.Comparison.Subheadline}}<p>{{.Config.Comparison.Subheadline}}</p>{{end}}
</div>
@@ -1542,8 +1565,8 @@
{{end}}
{{if .Config.Navigation.ShowRepository}}<a href="{{.RepoURL}}" class="ea-footer-link">Repository</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="ea-footer-link">Documentation</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ea-footer-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ea-footer-link">Issues</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="ea-footer-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="ea-footer-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
</div>
</footer>
</div>

View File

@@ -926,7 +926,19 @@
/* Responsive */
@media (max-width: 768px) {
.osh-nav { padding: 0 20px; }
.osh-nav-links { display: none; }
.osh-nav-links {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
padding: 16px 24px;
gap: 8px;
background: inherit;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 100;
}
.osh-menu-toggle { display: flex; }
.osh-hero { padding: 120px 24px 80px; }
.osh-hero-ctas { flex-direction: column; align-items: center; }
@@ -986,16 +998,17 @@
{{range .Config.Footer.Links}}
<a href="{{.URL}}" class="osh-nav-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="osh-nav-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="osh-nav-link">API</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="osh-nav-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="osh-nav-link">Issues</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="osh-nav-link">Why Us</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="osh-nav-link">Features</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="osh-nav-link">Pricing</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="osh-nav-link">Blog</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="osh-nav-link">Gallery</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="osh-nav-link">Compare</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="osh-nav-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="osh-nav-link">{{if .Config.Navigation.LabelAPI}}{{.Config.Navigation.LabelAPI}}{{else}}API{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="osh-nav-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="osh-nav-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="osh-nav-link">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why Us{{end}}</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="osh-nav-link">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="osh-nav-link">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="osh-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="osh-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="osh-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="osh-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
{{if .Config.Navigation.ShowRepository}}
<a href="{{.RepoURL}}" class="osh-nav-cta">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
@@ -1051,7 +1064,7 @@
<section class="osh-features" style="padding-top: 120px;">
<div class="osh-features-inner">
<div class="osh-section-header osh-reveal">
<div class="osh-section-label">Blog</div>
<div class="osh-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}All Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1082,7 +1095,7 @@
</section>
{{else}}
<!-- Hero Section -->
<section class="osh-hero">
<section class="osh-hero" id="hero">
<div class="osh-hero-gradient"></div>
<div class="osh-hero-content">
@@ -1103,11 +1116,6 @@
{{.Config.Hero.PrimaryCTA.Label}}
{{svg "octicon-arrow-right" 16}}
</a>
{{else}}
<a href="{{.RepoURL}}" class="osh-btn-primary" data-cta="primary">
Get Started
{{svg "octicon-arrow-right" 16}}
</a>
{{end}}
{{if .Config.Hero.SecondaryCTA.Label}}
@@ -1115,14 +1123,26 @@
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
{{.Config.Hero.SecondaryCTA.Label}}
</a>
{{else}}
<a href="{{.RepoURL}}" class="osh-btn-secondary" data-cta="secondary">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
View Source
</a>
{{end}}
</div>
{{if or $.GooglePlayID $.AppStoreID}}
<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 16px;" class="osh-reveal visible osh-reveal-delay-4">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="osh-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="osh-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
{{end}}
{{if .Config.Hero.CodeExample}}
<div class="osh-code-block osh-reveal visible osh-reveal-delay-4">
<span class="osh-code-prompt">$</span>
@@ -1137,7 +1157,7 @@
<!-- Stats Section -->
{{if or .Config.Stats (gt .NumStars 0)}}
<section class="osh-stats">
<section class="osh-stats" id="stats">
<div class="osh-stats-inner osh-reveal">
{{if .Config.Stats}}
{{range .Config.Stats}}
@@ -1168,7 +1188,7 @@
<!-- Downloads Section -->
{{if and .PublicReleases .LatestRelease .LatestRelease.Attachments}}
<section class="osh-downloads">
<section class="osh-downloads" id="downloads">
<div class="osh-downloads-inner osh-reveal">
<h2>Download v{{.LatestReleaseTag}}</h2>
<p>Get the latest release</p>
@@ -1231,25 +1251,6 @@
</div>
{{end}}
{{end}}
{{if or $.GooglePlayID $.AppStoreID}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 14px; color: var(--osh-muted); font-family: 'IBM Plex Mono', monospace; letter-spacing: 0.05em; text-transform: uppercase;">{{svg "octicon-device-mobile" 16}} App Stores</h4>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="osh-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="osh-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
</div>
{{end}}
{{if $otherFiles}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 14px; color: var(--osh-muted); font-family: 'IBM Plex Mono', monospace; letter-spacing: 0.05em; text-transform: uppercase;">{{svg "octicon-file" 16}} Other</h4>
@@ -1267,10 +1268,10 @@
<section class="osh-features" id="value-props">
<div class="osh-features-inner">
<div class="osh-section-header osh-reveal">
<div class="osh-section-label">Why choose us</div>
<h2>{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}</h2>
<p>Everything you need to get started quickly.</p>
</div>
<div class="osh-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose us{{end}}</div>
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
</div>
<div class="osh-features-grid">
{{range .Config.ValueProps}}
<div class="osh-feature-card osh-reveal">
@@ -1291,10 +1292,10 @@
<section class="osh-features" id="features" style="padding-top: 40px;">
<div class="osh-features-inner">
<div class="osh-section-header osh-reveal">
<div class="osh-section-label">Capabilities</div>
<h2>Features</h2>
<p>Powerful capabilities at your fingertips.</p>
</div>
<div class="osh-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
</div>
<div class="osh-features-grid">
{{range .Config.Features}}
<div class="osh-feature-card osh-reveal">
@@ -1312,7 +1313,7 @@
<!-- Social Proof -->
{{if or .Config.SocialProof.Logos .Config.SocialProof.Testimonials}}
<section class="osh-social-proof">
<section class="osh-social-proof" id="social-proof">
<div class="osh-social-proof-inner">
{{if .Config.SocialProof.Logos}}
<div class="osh-logos osh-reveal">
@@ -1352,7 +1353,7 @@
<section class="osh-pricing" id="pricing">
<div class="osh-pricing-inner">
<div class="osh-section-header osh-reveal">
<div class="osh-section-label">Pricing</div>
<div class="osh-section-label">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</div>
<h2>{{if .Config.Pricing.Headline}}{{.Config.Pricing.Headline}}{{else}}Pricing{{end}}</h2>
<p>{{if .Config.Pricing.Subheadline}}{{.Config.Pricing.Subheadline}}{{else}}Choose the plan that works for you{{end}}</p>
</div>
@@ -1376,9 +1377,35 @@
</section>
{{end}}
<!-- Cross-Promote Section -->
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
<section class="osh-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
<div class="osh-features-inner">
<div class="osh-section-header osh-reveal">
<div class="osh-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
</div>
<div class="osh-features-grid">
{{range .CrossPromoteItems}}
<a href="{{.URL}}" class="osh-feature-card osh-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
{{if .LogoURL}}
<div style="margin-bottom: 16px;">
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
</div>
{{end}}
<h3 class="osh-feature-title">{{.Name}}</h3>
{{if .Description}}<p class="osh-feature-desc">{{.Description}}</p>{{end}}
</a>
{{end}}
</div>
</div>
</section>
{{end}}
<!-- CTA Section -->
{{if .Config.CTASection.Headline}}
<section class="osh-cta-section">
<section class="osh-cta-section" id="cta">
<div class="osh-cta-inner osh-reveal">
<h2>{{.Config.CTASection.Headline}}</h2>
{{if .Config.CTASection.Subheadline}}
@@ -1397,7 +1424,7 @@
<section class="osh-features" id="blog" style="border-top: 1px solid rgba(255,255,255,0.04);">
<div class="osh-features-inner">
<div class="osh-section-header osh-reveal">
<div class="osh-section-label">Blog</div>
<div class="osh-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}Latest Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1436,7 +1463,7 @@
<section class="osh-features" id="gallery" style="border-top: 1px solid rgba(255,255,255,0.04);">
<div class="osh-features-inner">
<div class="osh-section-header osh-reveal">
<div class="osh-section-label">Gallery</div>
<div class="osh-section-label">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</div>
<h2>{{if .Config.Gallery.Headline}}{{.Config.Gallery.Headline}}{{else}}Gallery{{end}}</h2>
{{if .Config.Gallery.Subheadline}}<p>{{.Config.Gallery.Subheadline}}</p>{{end}}
</div>
@@ -1461,7 +1488,7 @@
<section class="osh-features" id="comparison" style="border-top: 1px solid rgba(255,255,255,0.04);">
<div class="osh-features-inner">
<div class="osh-section-header osh-reveal">
<div class="osh-section-label">Compare</div>
<div class="osh-section-label">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</div>
<h2>{{if .Config.Comparison.Headline}}{{.Config.Comparison.Headline}}{{else}}How We Compare{{end}}</h2>
{{if .Config.Comparison.Subheadline}}<p>{{.Config.Comparison.Subheadline}}</p>{{end}}
</div>
@@ -1544,8 +1571,8 @@
{{end}}
{{if .Config.Navigation.ShowRepository}}<a href="{{.RepoURL}}" class="osh-footer-link">Repository</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="osh-footer-link">Documentation</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="osh-footer-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="osh-footer-link">Issues</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="osh-footer-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="osh-footer-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
</div>
</footer>
</div>

View File

@@ -30,6 +30,7 @@
.gm-reveal-delay-1 { transition-delay: 0.12s; }
.gm-reveal-delay-2 { transition-delay: 0.24s; }
.gm-reveal-delay-3 { transition-delay: 0.36s; }
.gm-reveal-delay-4 { transition-delay: 0.48s; }
.gm-page {
min-height: 100vh;
@@ -1042,7 +1043,19 @@
@media (max-width: 768px) {
.gm-nav { top: 8px; left: 8px; right: 8px; padding: 12px 20px; }
.gm-nav-links { display: none; }
.gm-nav-links {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
padding: 16px 24px;
gap: 8px;
background: inherit;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 100;
}
.gm-mobile-toggle { display: block; }
.gm-hero { padding: 120px 24px 80px; }
.gm-hero h1 { font-size: 36px; }
@@ -1103,16 +1116,17 @@
{{range .Config.Footer.Links}}
<a href="{{.URL}}" class="gm-nav-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="gm-nav-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="gm-nav-link">API</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="gm-nav-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="gm-nav-link">Issues</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="gm-nav-link">Why</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="gm-nav-link">Features</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="gm-nav-link">Pricing</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="gm-nav-link">Blog</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="gm-nav-link">Gallery</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="gm-nav-link">Compare</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="gm-nav-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="gm-nav-link">{{if .Config.Navigation.LabelAPI}}{{.Config.Navigation.LabelAPI}}{{else}}API{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="gm-nav-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="gm-nav-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="gm-nav-link">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why{{end}}</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="gm-nav-link">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="gm-nav-link">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="gm-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="gm-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="gm-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="gm-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
{{if .Config.Navigation.ShowRepository}}
<a href="{{.RepoURL}}" class="gm-nav-repo">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
@@ -1131,9 +1145,11 @@
</div>
</div>
{{end}}
<a href="{{if .Config.Hero.PrimaryCTA.URL}}{{.Config.Hero.PrimaryCTA.URL}}{{else}}{{.RepoURL}}{{end}}" class="gm-btn-primary" data-cta="primary" style="padding: 10px 20px; font-size: 13px;">
<span>{{if .Config.Hero.PrimaryCTA.Label}}{{.Config.Hero.PrimaryCTA.Label}}{{else}}Get Started{{end}}</span>
{{if .Config.Hero.PrimaryCTA.Label}}
<a href="{{.Config.Hero.PrimaryCTA.URL}}" class="gm-btn-primary" data-cta="primary" style="padding: 10px 20px; font-size: 13px;">
<span>{{.Config.Hero.PrimaryCTA.Label}}</span>
</a>
{{end}}
</div>
<button class="gm-mobile-toggle" onclick="document.getElementById('gm-mobile-nav').classList.toggle('open')">Menu</button>
</nav>
@@ -1141,16 +1157,17 @@
{{range .Config.Footer.Links}}
<a href="{{.URL}}" class="gm-nav-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="gm-nav-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="gm-nav-link">API</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="gm-nav-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="gm-nav-link">Issues</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="gm-nav-link">Why</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="gm-nav-link">Features</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="gm-nav-link">Pricing</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="gm-nav-link">Blog</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="gm-nav-link">Gallery</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="gm-nav-link">Compare</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="gm-nav-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="gm-nav-link">{{if .Config.Navigation.LabelAPI}}{{.Config.Navigation.LabelAPI}}{{else}}API{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="gm-nav-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="gm-nav-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="gm-nav-link">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why{{end}}</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="gm-nav-link">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="gm-nav-link">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="gm-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="gm-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="gm-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="gm-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
{{if .Config.Navigation.ShowRepository}}
<a href="{{.RepoURL}}" class="gm-nav-repo">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
@@ -1190,7 +1207,7 @@
<section class="gm-section" style="padding-top: 120px;">
<div class="gm-section-inner">
<div class="gm-section-header gm-reveal">
<div class="gm-section-eyebrow">Blog</div>
<div class="gm-section-eyebrow">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}All Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1221,7 +1238,7 @@
</section>
{{else}}
<!-- Hero Section -->
<section class="gm-hero">
<section class="gm-hero" id="hero">
<div class="gm-hero-inner">
<div>
{{if gt .NumStars 100}}
@@ -1238,12 +1255,14 @@
</p>
<div class="gm-hero-ctas gm-reveal gm-reveal-delay-3">
<a href="{{if .Config.Hero.PrimaryCTA.URL}}{{.Config.Hero.PrimaryCTA.URL}}{{else}}{{.RepoURL}}{{end}}" class="gm-btn-primary" data-cta="primary">
{{if .Config.Hero.PrimaryCTA.Label}}
<a href="{{.Config.Hero.PrimaryCTA.URL}}" class="gm-btn-primary" data-cta="primary">
<span>
{{if .Config.Hero.PrimaryCTA.Label}}{{.Config.Hero.PrimaryCTA.Label}}{{else}}Get Started{{end}}
{{.Config.Hero.PrimaryCTA.Label}}
{{svg "octicon-arrow-right" 16}}
</span>
</a>
{{end}}
{{if .Config.Hero.SecondaryCTA.Label}}
<a href="{{.Config.Hero.SecondaryCTA.URL}}" class="gm-btn-secondary" data-cta="secondary">
{{svg "octicon-play" 16}}
@@ -1252,6 +1271,23 @@
{{end}}
</div>
{{if or $.GooglePlayID $.AppStoreID}}
<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 16px;" class="gm-reveal gm-reveal-delay-4">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="gm-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="gm-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
{{end}}
<div class="gm-hero-note gm-reveal gm-reveal-delay-3">
<span>{{svg "octicon-check" 14}} Free to use</span>
<span>{{svg "octicon-check" 14}} Open source</span>
@@ -1280,7 +1316,7 @@
<!-- Stats Section -->
{{if .Config.Stats}}
<section class="gm-stats">
<section class="gm-stats" id="stats">
<div class="gm-stats-grid">
{{range .Config.Stats}}
<div class="gm-stat-card gm-reveal">
@@ -1293,7 +1329,7 @@
{{end}}
{{if and .PublicReleases .LatestRelease .LatestRelease.Attachments}}
<section class="gm-downloads">
<section class="gm-downloads" id="downloads">
<div class="gm-downloads-inner">
<div class="gm-downloads-header gm-reveal">
<h2>Download v{{.LatestReleaseTag}}</h2>
@@ -1358,25 +1394,6 @@
</div>
{{end}}
{{end}}
{{if or $.GooglePlayID $.AppStoreID}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 14px; color: var(--gm-light); font-weight: 600;">{{svg "octicon-device-mobile" 16}} App Stores</h4>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="gm-download-item gm-reveal" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="gm-download-item gm-reveal" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
</div>
{{end}}
{{if $otherFiles}}
<div style="margin-bottom: 24px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 14px; color: var(--gm-light); font-weight: 600;">{{svg "octicon-file" 16}} Other</h4>
@@ -1391,7 +1408,7 @@
<!-- Trust Bar -->
{{if .Config.SocialProof.Logos}}
<section class="gm-trust">
<section class="gm-trust" id="social-proof">
<p class="gm-trust-label gm-reveal">Trusted by teams at</p>
<div class="gm-trust-bar gm-reveal gm-reveal-delay-1">
{{range .Config.SocialProof.Logos}}
@@ -1406,8 +1423,8 @@
<section class="gm-section" id="value-props">
<div class="gm-section-inner">
<div class="gm-section-header gm-reveal">
<h2>Why <span class="gm-serif">{{if .Config.Brand.Name}}{{.Config.Brand.Name}}{{else}}{{.Repository.Name}}{{end}}</span></h2>
<p>Built for developers who value their time</p>
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}Why <span class="gm-serif">{{if .Config.Brand.Name}}{{.Config.Brand.Name}}{{else}}{{.Repository.Name}}{{end}}</span>{{end}}</h2>
<p>{{if .Config.ValuePropsSubheadline}}{{.Config.ValuePropsSubheadline}}{{else}}Built for developers who value their time{{end}}</p>
</div>
<div class="gm-value-grid">
@@ -1427,8 +1444,8 @@
<section id="features" class="gm-section">
<div class="gm-section-inner">
<div class="gm-section-header gm-reveal">
<h2>How it <span class="gm-serif">works</span></h2>
<p>Get started in minutes, not weeks</p>
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}How it <span class="gm-serif">works</span>{{end}}</h2>
<p>{{if .Config.FeaturesSubheadline}}{{.Config.FeaturesSubheadline}}{{else}}Get started in minutes, not weeks{{end}}</p>
</div>
<div class="gm-features-list">
@@ -1450,7 +1467,7 @@
<!-- Testimonials -->
{{if .Config.SocialProof.Testimonials}}
<section class="gm-section">
<section class="gm-section" id="testimonials">
<div class="gm-testimonial-card">
<div class="gm-testimonials-container">
{{range .Config.SocialProof.Testimonials}}
@@ -1518,7 +1535,7 @@
<!-- Final CTA -->
{{if .Config.CTASection.Headline}}
<section class="gm-section">
<section class="gm-section" id="cta">
<div class="gm-cta-gradient gm-reveal">
<h2>{{.Config.CTASection.Headline}}</h2>
{{if .Config.CTASection.Subheadline}}
@@ -1601,7 +1618,7 @@
<section class="gm-section" id="comparison" style="border-top: 1px solid rgba(255,255,255,0.06);">
<div class="gm-section-inner">
<div class="gm-section-header gm-reveal">
<div class="gm-section-label">Compare</div>
<div class="gm-section-label">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</div>
<h2>{{if .Config.Comparison.Headline}}{{.Config.Comparison.Headline}}{{else}}How We Compare{{end}}</h2>
{{if .Config.Comparison.Subheadline}}<p>{{.Config.Comparison.Subheadline}}</p>{{end}}
</div>
@@ -1648,6 +1665,32 @@
{{end}}{{/* end PageIsBlogDetail / PageIsBlogList / else */}}
<!-- Cross-Promote Section -->
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
<section class="gm-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
<div class="gm-features-inner">
<div class="gm-section-header gm-reveal">
<div class="gm-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
</div>
<div class="gm-features-grid">
{{range .CrossPromoteItems}}
<a href="{{.URL}}" class="gm-feature-card gm-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
{{if .LogoURL}}
<div style="margin-bottom: 16px;">
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
</div>
{{end}}
<h3 class="gm-feature-title">{{.Name}}</h3>
{{if .Description}}<p class="gm-feature-desc">{{.Description}}</p>{{end}}
</a>
{{end}}
</div>
</div>
</section>
{{end}}
<!-- Footer -->
<footer class="gm-footer">
<div class="gm-footer-brand">
@@ -1683,9 +1726,9 @@
<a href="{{.URL}}" class="gm-footer-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowRepository}}<a href="{{.RepoURL}}" class="gm-footer-link">Repository</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="gm-footer-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="gm-footer-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="gm-footer-link">Issues</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="gm-footer-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="gm-footer-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="gm-footer-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
</div>
</footer>
</div>

View File

@@ -979,7 +979,19 @@
/* Responsive */
@media (max-width: 768px) {
.vs-nav { padding: 0 20px; }
.vs-nav-links { display: none; }
.vs-nav-links {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
padding: 16px 24px;
gap: 8px;
background: inherit;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 100;
}
.vs-menu-toggle { display: flex; }
.vs-hero { padding: 120px 24px 80px; }
.vs-hero-ctas { flex-direction: column; align-items: center; }
@@ -1022,16 +1034,17 @@
{{range .Config.Footer.Links}}
<a href="{{.URL}}" class="vs-nav-link">{{.Label}}</a>
{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="vs-nav-link">Docs</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="vs-nav-link">API</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="vs-nav-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="vs-nav-link">Issues</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="vs-nav-link">Value Props</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="vs-nav-link">Features</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="vs-nav-link">Pricing</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="vs-nav-link">Blog</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="vs-nav-link">Gallery</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="vs-nav-link">Compare</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="vs-nav-link">{{if .Config.Navigation.LabelDocs}}{{.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</a>{{end}}
{{if .Config.Navigation.ShowAPI}}<a href="{{.RepoURL}}/swagger" class="vs-nav-link">{{if .Config.Navigation.LabelAPI}}{{.Config.Navigation.LabelAPI}}{{else}}API{{end}}</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="vs-nav-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="vs-nav-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
{{if .Config.ValueProps}}<a href="{{.LandingURL}}#value-props" class="vs-nav-link">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Value Props{{end}}</a>{{end}}
{{if .Config.Features}}<a href="{{.LandingURL}}#features" class="vs-nav-link">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Features{{end}}</a>{{end}}
{{if .Config.Pricing.Plans}}<a href="{{.LandingURL}}#pricing" class="vs-nav-link">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</a>{{end}}
{{if .Config.Blog.Enabled}}<a href="{{if .BlogBaseURL}}{{.BlogBaseURL}}{{else}}{{.LandingURL}}#blog{{end}}" class="vs-nav-link">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</a>{{end}}
{{if .Config.Gallery.Enabled}}<a href="{{.LandingURL}}#gallery" class="vs-nav-link">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</a>{{end}}
{{if and .Config.Comparison.Enabled .Config.Comparison.HasData}}<a href="{{.LandingURL}}#comparison" class="vs-nav-link">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</a>{{end}}
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}<a href="{{.LandingURL}}#cross-promote" class="vs-nav-link">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</a>{{end}}
{{if .Config.Navigation.ShowRepository}}
<a href="{{.RepoURL}}" class="vs-nav-cta">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
@@ -1087,7 +1100,7 @@
<section class="vs-features" style="padding-top: 140px;">
<div class="vs-features-inner">
<div class="vs-section-header vs-reveal">
<div class="vs-section-label">Blog</div>
<div class="vs-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}All Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1118,7 +1131,7 @@
</section>
{{else}}
<!-- Hero Section -->
<section class="vs-hero">
<section class="vs-hero" id="hero">
<div class="vs-hero-mesh">
<div class="vs-hero-blob-1"></div>
<div class="vs-hero-blob-2"></div>
@@ -1143,11 +1156,6 @@
{{.Config.Hero.PrimaryCTA.Label}}
{{svg "octicon-arrow-right" 16}}
</a>
{{else}}
<a href="{{.RepoURL}}" class="vs-btn-primary" data-cta="primary">
Get Started
{{svg "octicon-arrow-right" 16}}
</a>
{{end}}
{{if .Config.Hero.SecondaryCTA.Label}}
@@ -1155,14 +1163,26 @@
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
{{.Config.Hero.SecondaryCTA.Label}}
</a>
{{else}}
<a href="{{.RepoURL}}" class="vs-btn-secondary" data-cta="secondary">
<img src="/assets/img/gitcaddy-icon.svg" width="16" height="16" alt="GitCaddy">
View Source
</a>
{{end}}
</div>
{{if or $.GooglePlayID $.AppStoreID}}
<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 16px;" class="vs-reveal visible vs-reveal-delay-4">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="vs-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="vs-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
{{end}}
{{if .Config.Hero.CodeExample}}
<div class="vs-code-block vs-reveal visible vs-reveal-delay-4">
<span class="vs-code-prompt">$</span>
@@ -1177,7 +1197,7 @@
<!-- Stats Section -->
{{if or .Config.Stats (gt .NumStars 0)}}
<section class="vs-stats">
<section class="vs-stats" id="stats">
<div class="vs-stats-inner vs-reveal">
{{if .Config.Stats}}
{{range .Config.Stats}}
@@ -1208,7 +1228,7 @@
<!-- Downloads Section -->
{{if and .PublicReleases .LatestRelease .LatestRelease.Attachments}}
<section class="vs-downloads">
<section class="vs-downloads" id="downloads">
<div class="vs-downloads-inner vs-reveal">
<h2>Download v{{.LatestReleaseTag}}</h2>
<p>Get the latest release</p>
@@ -1271,25 +1291,6 @@
</div>
{{end}}
{{end}}
{{if or $.GooglePlayID $.AppStoreID}}
<div style="margin-bottom: 28px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 14px; font-size: 13px; color: var(--vs-muted); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600;">{{svg "octicon-device-mobile" 16}} App Stores</h4>
<div style="display: flex; gap: 14px; flex-wrap: wrap; justify-content: center;">
{{if $.GooglePlayID}}
<a href="https://play.google.com/store/apps/details?id={{$.GooglePlayID}}" target="_blank" rel="noopener" class="vs-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-1.4l2.834 1.64a1 1 0 0 1 0 1.726l-2.834 1.64-2.635-2.636 2.635-2.37zM5.864 2.658L16.8 9.99l-2.302 2.302-8.635-8.635z"/></svg>
Google Play
</a>
{{end}}
{{if $.AppStoreID}}
<a href="https://apps.apple.com/app/{{$.AppStoreID}}" target="_blank" rel="noopener" class="vs-download-item" style="display: inline-flex; align-items: center; gap: 10px;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
App Store
</a>
{{end}}
</div>
</div>
{{end}}
{{if $otherFiles}}
<div style="margin-bottom: 28px;">
<h4 style="display: flex; align-items: center; gap: 8px; margin-bottom: 14px; font-size: 13px; color: var(--vs-muted); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600;">{{svg "octicon-file" 16}} Other</h4>
@@ -1307,10 +1308,10 @@
<section class="vs-features" id="value-props" style="background: var(--vs-surface);">
<div class="vs-features-inner">
<div class="vs-section-header vs-reveal">
<div class="vs-section-label">Why choose us</div>
<h2>{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}</h2>
<p>Everything you need to get started quickly.</p>
</div>
<div class="vs-section-label">{{if .Config.Navigation.LabelValueProps}}{{.Config.Navigation.LabelValueProps}}{{else}}Why choose us{{end}}</div>
<h2>{{if .Config.ValuePropsHeadline}}{{.Config.ValuePropsHeadline}}{{else}}{{if .Config.Brand.Name}}Why {{.Config.Brand.Name}}?{{else}}Why Choose Us{{end}}{{end}}</h2>
{{if .Config.ValuePropsSubheadline}}<p>{{.Config.ValuePropsSubheadline}}</p>{{end}}
</div>
<div class="vs-features-grid">
{{range .Config.ValueProps}}
<div class="vs-feature-card vs-reveal">
@@ -1331,10 +1332,10 @@
<section class="vs-features" id="features" style="padding-top: 40px;">
<div class="vs-features-inner">
<div class="vs-section-header vs-reveal">
<div class="vs-section-label">Capabilities</div>
<h2>Features</h2>
<p>Powerful capabilities at your fingertips.</p>
</div>
<div class="vs-section-label">{{if .Config.Navigation.LabelFeatures}}{{.Config.Navigation.LabelFeatures}}{{else}}Capabilities{{end}}</div>
<h2>{{if .Config.FeaturesHeadline}}{{.Config.FeaturesHeadline}}{{else}}Features{{end}}</h2>
{{if .Config.FeaturesSubheadline}}<p>{{.Config.FeaturesSubheadline}}</p>{{end}}
</div>
<div class="vs-features-grid">
{{range .Config.Features}}
<div class="vs-feature-card vs-reveal">
@@ -1352,7 +1353,7 @@
<!-- Social Proof -->
{{if or .Config.SocialProof.Logos .Config.SocialProof.Testimonials}}
<section class="vs-social-proof">
<section class="vs-social-proof" id="social-proof">
<div class="vs-social-proof-inner">
{{if .Config.SocialProof.Logos}}
<div class="vs-logos vs-reveal">
@@ -1392,7 +1393,7 @@
<section class="vs-pricing" id="pricing">
<div class="vs-pricing-inner">
<div class="vs-section-header vs-reveal">
<div class="vs-section-label">Pricing</div>
<div class="vs-section-label">{{if .Config.Navigation.LabelPricing}}{{.Config.Navigation.LabelPricing}}{{else}}Pricing{{end}}</div>
<h2>{{if .Config.Pricing.Headline}}{{.Config.Pricing.Headline}}{{else}}Pricing{{end}}</h2>
<p>{{if .Config.Pricing.Subheadline}}{{.Config.Pricing.Subheadline}}{{else}}Choose the plan that works for you{{end}}</p>
</div>
@@ -1416,9 +1417,35 @@
</section>
{{end}}
<!-- Cross-Promote Section -->
{{if and .Config.CrossPromote.Enabled .CrossPromoteItems}}
<section class="vs-features" id="cross-promote" style="border-top: 1px solid rgba(255,255,255,0.04);">
<div class="vs-features-inner">
<div class="vs-section-header vs-reveal">
<div class="vs-section-label">{{if .Config.Navigation.LabelCrossPromote}}{{.Config.Navigation.LabelCrossPromote}}{{else}}Related Offerings{{end}}</div>
<h2>{{if .Config.CrossPromote.Headline}}{{.Config.CrossPromote.Headline}}{{else}}Related Offerings{{end}}</h2>
{{if .Config.CrossPromote.Subheadline}}<p>{{.Config.CrossPromote.Subheadline}}</p>{{end}}
</div>
<div class="vs-features-grid">
{{range .CrossPromoteItems}}
<a href="{{.URL}}" class="vs-feature-card vs-reveal" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; text-align: center;">
{{if .LogoURL}}
<div style="margin-bottom: 16px;">
<img src="{{.LogoURL}}" alt="{{.Name}}" style="max-width: 120px; max-height: 60px; object-fit: contain;">
</div>
{{end}}
<h3 class="vs-feature-title">{{.Name}}</h3>
{{if .Description}}<p class="vs-feature-desc">{{.Description}}</p>{{end}}
</a>
{{end}}
</div>
</div>
</section>
{{end}}
<!-- CTA Section -->
{{if .Config.CTASection.Headline}}
<section class="vs-cta-section">
<section class="vs-cta-section" id="cta">
<div class="vs-cta-inner vs-reveal">
<h2>{{.Config.CTASection.Headline}}</h2>
{{if .Config.CTASection.Subheadline}}
@@ -1437,7 +1464,7 @@
<section class="vs-features" id="blog" style="border-top: 1px solid var(--vs-border); background: var(--vs-surface);">
<div class="vs-features-inner">
<div class="vs-section-header vs-reveal">
<div class="vs-section-label">Blog</div>
<div class="vs-section-label">{{if .Config.Navigation.LabelBlog}}{{.Config.Navigation.LabelBlog}}{{else}}Blog{{end}}</div>
<h2>{{if .Config.Blog.Headline}}{{.Config.Blog.Headline}}{{else}}Latest Posts{{end}}</h2>
{{if .Config.Blog.Subheadline}}<p>{{.Config.Blog.Subheadline}}</p>{{end}}
</div>
@@ -1476,7 +1503,7 @@
<section class="vs-features" id="gallery" style="border-top: 1px solid var(--vs-border);">
<div class="vs-features-inner">
<div class="vs-section-header vs-reveal">
<div class="vs-section-label">Gallery</div>
<div class="vs-section-label">{{if .Config.Navigation.LabelGallery}}{{.Config.Navigation.LabelGallery}}{{else}}Gallery{{end}}</div>
<h2>{{if .Config.Gallery.Headline}}{{.Config.Gallery.Headline}}{{else}}Gallery{{end}}</h2>
{{if .Config.Gallery.Subheadline}}<p>{{.Config.Gallery.Subheadline}}</p>{{end}}
</div>
@@ -1501,7 +1528,7 @@
<section class="vs-features" id="comparison" style="border-top: 1px solid var(--vs-border);">
<div class="vs-features-inner">
<div class="vs-section-header vs-reveal">
<div class="vs-section-label">Compare</div>
<div class="vs-section-label">{{if .Config.Navigation.LabelCompare}}{{.Config.Navigation.LabelCompare}}{{else}}Compare{{end}}</div>
<h2>{{if .Config.Comparison.Headline}}{{.Config.Comparison.Headline}}{{else}}How We Compare{{end}}</h2>
{{if .Config.Comparison.Subheadline}}<p>{{.Config.Comparison.Subheadline}}</p>{{end}}
</div>
@@ -1584,8 +1611,8 @@
{{end}}
{{if .Config.Navigation.ShowRepository}}<a href="{{.RepoURL}}" class="vs-footer-link">Repository</a>{{end}}
{{if .Config.Navigation.ShowDocs}}<a href="{{.RepoURL}}/wiki" class="vs-footer-link">Documentation</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="vs-footer-link">Releases</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="vs-footer-link">Issues</a>{{end}}
{{if .Config.Navigation.ShowReleases}}<a href="{{.RepoURL}}/releases" class="vs-footer-link">{{if .Config.Navigation.LabelReleases}}{{.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</a>{{end}}
{{if .Config.Navigation.ShowIssues}}<a href="{{.RepoURL}}/issues" class="vs-footer-link">{{if .Config.Navigation.LabelIssues}}{{.Config.Navigation.LabelIssues}}{{else}}Issues{{end}}</a>{{end}}
</div>
</footer>
</div>

View File

@@ -53,7 +53,7 @@
</a>
{{end}}
{{end}}
<details class="item toggleable-item" {{if or .PageIsSettingsPagesGeneral .PageIsSettingsPagesBrand .PageIsSettingsPagesHero .PageIsSettingsPagesContent .PageIsSettingsPagesSocial .PageIsSettingsPagesPricing .PageIsSettingsPagesFooter .PageIsSettingsPagesTheme}}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">
@@ -68,6 +68,9 @@
<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>
@@ -80,6 +83,12 @@
<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>
</details>
<details class="item toggleable-item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables .PageIsActionsSettingsGeneral}}open{{end}}>

View File

@@ -285,6 +285,15 @@
<label for="push_mirror_sync_on_commit">{{ctx.Locale.Tr "repo.mirror_sync_on_commit"}}</label>
</div>
</div>
{{if .Repository.HideDotfiles}}
<div class="field">
<div class="ui checkbox">
<input id="push_mirror_exclude_hidden_files" name="push_mirror_exclude_hidden_files" type="checkbox" checked>
<label for="push_mirror_exclude_hidden_files">{{ctx.Locale.Tr "repo.settings.mirror_settings.push_mirror.exclude_hidden_files"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "repo.settings.mirror_settings.push_mirror.exclude_hidden_files_desc"}}</p>
</div>
{{end}}
<div class="inline field {{if .Err_PushMirrorInterval}}error{{end}}">
<label for="push_mirror_interval">{{ctx.Locale.Tr "repo.mirror_interval" .MinimumMirrorInterval}}</label>
<input id="push_mirror_interval" name="push_mirror_interval" value="{{if .push_mirror_interval}}{{.push_mirror_interval}}{{else}}{{.DefaultMirrorInterval}}{{end}}">

View File

@@ -29,15 +29,19 @@
{{if .AIEnabled}}
<div class="divider"></div>
<form class="ui form" method="post">
<form class="ui form" method="post" id="ai-generate-form">
{{.CsrfTokenHtml}}
<input type="hidden" name="action" value="ai_generate">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.ai_generate"}}</label>
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.ai_generate_desc"}}</p>
</div>
<div class="field">
<button class="ui purple button">{{ctx.Locale.Tr "repo.settings.pages.ai_generate_button"}}</button>
<div class="field" id="ai-generate-btn-field">
<button class="ui purple button" type="submit">{{svg "octicon-copilot" 16}} {{ctx.Locale.Tr "repo.settings.pages.ai_generate_button"}}</button>
</div>
<div id="ai-generate-loading" class="tw-hidden tw-py-4">
<div class="ui active centered inline loader"></div>
<p class="tw-text-center tw-mt-2" style="color:var(--color-text-light);">{{ctx.Locale.Tr "repo.settings.pages.ai_generating"}}</p>
</div>
</form>
{{end}}
@@ -169,4 +173,17 @@
</div>
{{end}}
</div>
<script>
(function() {
var form = document.getElementById('ai-generate-form');
if (!form) return;
form.addEventListener('submit', function() {
var btn = form.querySelector('button[type="submit"]');
btn.classList.add('loading', 'disabled');
btn.disabled = true;
document.getElementById('ai-generate-btn-field').classList.add('tw-hidden');
document.getElementById('ai-generate-loading').classList.remove('tw-hidden');
});
})();
</script>
{{template "repo/settings/layout_footer" .}}

View File

@@ -0,0 +1,82 @@
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings pages")}}
<div class="user-main-content twelve wide column">
<h4 class="ui top attached header">{{ctx.Locale.Tr "repo.settings.pages.advanced"}}</h4>
<div class="ui attached segment">
<form class="ui form" method="post">
{{.CsrfTokenHtml}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.static_routes"}}</h5>
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.static_routes_desc"}}</p>
<div id="routes-container">
{{range $i, $route := .Config.Advanced.StaticRoutes}}
<div class="field route-item tw-flex tw-items-center tw-gap-2">
<input name="static_route_{{$i}}" value="{{$route}}" placeholder="/schema/*" class="tw-flex-1">
<button type="button" class="ui red mini icon button" onclick="this.closest('.route-item').remove()">{{svg "octicon-trash" 14}}</button>
</div>
{{end}}
{{if not .Config.Advanced.StaticRoutes}}
<div class="field route-item tw-flex tw-items-center tw-gap-2">
<input name="static_route_0" placeholder="/schema/*" class="tw-flex-1">
<button type="button" class="ui red mini icon button" onclick="this.closest('.route-item').remove()">{{svg "octicon-trash" 14}}</button>
</div>
{{end}}
</div>
<button type="button" class="ui mini button tw-mb-4" onclick="addRoute()">+ {{ctx.Locale.Tr "repo.settings.pages.add_route"}}</button>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.redirects"}}</h5>
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.redirects_desc"}}</p>
<div id="redirects-container">
{{range $path, $target := .Config.Advanced.Redirects}}
<div class="two fields redirect-item">
<div class="field"><input name="redirect_from_{{$path}}" value="{{$path}}" placeholder="/old-path"></div>
<div class="field tw-flex tw-items-center tw-gap-2">
<input name="redirect_to_{{$path}}" value="{{$target}}" placeholder="https://new-url" class="tw-flex-1">
<button type="button" class="ui red mini icon button" onclick="this.closest('.redirect-item').remove()">{{svg "octicon-trash" 14}}</button>
</div>
</div>
{{end}}
{{if not .Config.Advanced.Redirects}}
<div class="two fields redirect-item">
<div class="field"><input name="redirect_from_0" placeholder="/old-path"></div>
<div class="field tw-flex tw-items-center tw-gap-2">
<input name="redirect_to_0" placeholder="https://new-url" class="tw-flex-1">
<button type="button" class="ui red mini icon button" onclick="this.closest('.redirect-item').remove()">{{svg "octicon-trash" 14}}</button>
</div>
</div>
{{end}}
</div>
<button type="button" class="ui mini button tw-mb-4" onclick="addRedirect()">+ {{ctx.Locale.Tr "repo.settings.pages.add_redirect"}}</button>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.custom_code"}}</h5>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.custom_css"}}</label>
<textarea name="custom_css" rows="4" placeholder="body { }">{{.Config.Advanced.CustomCSS}}</textarea>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.custom_head"}}</label>
<textarea name="custom_head" rows="4" placeholder="<meta ...>">{{.Config.Advanced.CustomHead}}</textarea>
</div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</div>
</form>
</div>
</div>
<script>
let routeCount = {{len .Config.Advanced.StaticRoutes}};
let redirectCount = {{len .Config.Advanced.Redirects}};
if (routeCount === 0) routeCount = 1;
if (redirectCount === 0) redirectCount = 1;
function addRoute() {
const c = document.getElementById('routes-container');
c.insertAdjacentHTML('beforeend', `<div class="field route-item tw-flex tw-items-center tw-gap-2"><input name="static_route_${routeCount}" placeholder="/schema/*" class="tw-flex-1"><button type="button" class="ui red mini icon button" onclick="this.closest('.route-item').remove()">{{svg "octicon-trash" 14}}</button></div>`);
routeCount++;
}
function addRedirect() {
const c = document.getElementById('redirects-container');
c.insertAdjacentHTML('beforeend', `<div class="two fields redirect-item"><div class="field"><input name="redirect_from_${redirectCount}" placeholder="/old-path"></div><div class="field tw-flex tw-items-center tw-gap-2"><input name="redirect_to_${redirectCount}" placeholder="https://new-url" class="tw-flex-1"><button type="button" class="ui red mini icon button" onclick="this.closest('.redirect-item').remove()">{{svg "octicon-trash" 14}}</button></div></div>`);
redirectCount++;
}
</script>
{{template "repo/settings/layout_footer" .}}

View File

@@ -26,7 +26,7 @@
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.brand_upload_logo"}}</label>
<div class="tw-flex tw-gap-2 tw-items-center">
<input type="file" name="brand_logo" accept="image/jpeg,image/png,image/webp,image/gif" form="upload-logo-form">
<input type="file" name="brand_logo" accept="image/jpeg,image/png,image/webp,image/gif" form="upload-logo-form" data-ays-ignore="true">
<button class="ui primary button" type="submit" form="upload-logo-form">
{{svg "octicon-upload" 16}} {{ctx.Locale.Tr "repo.settings.pages.brand_upload_btn"}}
</button>
@@ -60,7 +60,7 @@
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.brand_upload_favicon"}}</label>
<div class="tw-flex tw-gap-2 tw-items-center">
<input type="file" name="brand_favicon" accept="image/jpeg,image/png,image/webp,image/gif,image/x-icon,image/svg+xml" form="upload-favicon-form">
<input type="file" name="brand_favicon" accept="image/jpeg,image/png,image/webp,image/gif,image/x-icon,image/svg+xml" form="upload-favicon-form" data-ays-ignore="true">
<button class="ui primary button" type="submit" form="upload-favicon-form">
{{svg "octicon-upload" 16}} {{ctx.Locale.Tr "repo.settings.pages.brand_upload_btn"}}
</button>

View File

@@ -123,7 +123,23 @@
<label>{{ctx.Locale.Tr "repo.settings.pages.comparison_enabled_desc"}}</label>
</div>
</div>
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.comparison_help_link"}}</p>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.cross_promote_section"}}</h5>
<div class="inline field">
<div class="ui toggle checkbox">
<input type="checkbox" name="cross_promote_enabled" {{if .Config.CrossPromote.Enabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pages.cross_promote_enabled_desc"}}</label>
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.cross_promote_headline"}}</label>
<input name="cross_promote_headline" value="{{.Config.CrossPromote.Headline}}" placeholder="Related Offerings">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.cross_promote_subheadline"}}</label>
<input name="cross_promote_subheadline" value="{{.Config.CrossPromote.Subheadline}}" placeholder="Explore our other projects">
</div>
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.cross_promote_help"}}</p>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.stats"}}</h5>
<div id="stats-container">
@@ -143,6 +159,22 @@
<button type="button" class="ui mini button" onclick="addStat()">+ Add Stat</button>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.value_props"}}</h5>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.label_value_props"}}</label>
<input name="label_value_props" value="{{.Config.Navigation.LabelValueProps}}" placeholder="Why choose us">
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.label_value_props_help"}}</p>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.value_props_headline"}}</label>
<input name="value_props_headline" value="{{.Config.ValuePropsHeadline}}" placeholder="Built for makers">
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.value_props_headline_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.value_props_subheadline"}}</label>
<input name="value_props_subheadline" value="{{.Config.ValuePropsSubheadline}}" placeholder="A short tagline beneath the headline">
</div>
</div>
<div id="valueprops-container">
{{range $i, $vp := .Config.ValueProps}}
<div class="fields valueprop-item">
@@ -210,6 +242,22 @@
<button type="button" class="ui mini button" onclick="addValueProp()">+ Add Value Prop</button>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.features"}}</h5>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.label_features"}}</label>
<input name="label_features" value="{{.Config.Navigation.LabelFeatures}}" placeholder="Capabilities">
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.label_features_help"}}</p>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.features_headline"}}</label>
<input name="features_headline" value="{{.Config.FeaturesHeadline}}" placeholder="Everything you need">
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.features_headline_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.features_subheadline"}}</label>
<input name="features_subheadline" value="{{.Config.FeaturesSubheadline}}" placeholder="A short tagline beneath the headline">
</div>
</div>
<div id="features-container">
{{range $i, $f := .Config.Features}}
<div class="fields feature-item">

View File

@@ -25,7 +25,7 @@
{{range $code, $name := .LanguageNames}}
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="languages" value="{{$code}}" {{if $.IsLangEnabled $code}}checked{{end}}>
<input type="checkbox" name="languages" value="{{$code}}" {{if index $.EnabledLangs $code}}checked{{end}}>
<label>{{$name}} ({{$code}})</label>
</div>
</div>
@@ -44,31 +44,44 @@
{{ctx.Locale.Tr "repo.settings.pages.translations"}}
</h4>
<div class="ui attached segment">
{{/* Action bar: language selector + buttons */}}
<div class="tw-flex tw-flex-wrap tw-gap-2 tw-items-center tw-mb-4">
<select id="lang-selector" class="ui dropdown" style="min-width:200px;">
{{range .Config.I18n.Languages}}
{{if ne . $.Config.I18n.DefaultLang}}
<option value="{{.}}">{{index $.LanguageNames .}} ({{.}}) {{if index $.TranslationMap .}}{{end}}</option>
{{end}}
{{end}}
</select>
{{if .AIEnabled}}
<form method="post" class="tw-inline-block" id="ai-translate-form">
{{.CsrfTokenHtml}}
<input type="hidden" name="action" value="ai_translate">
<input type="hidden" name="target_lang" id="ai-translate-lang" value="">
<button class="ui purple tiny button" type="submit">{{svg "octicon-copilot" 14}} {{ctx.Locale.Tr "repo.settings.pages.ai_translate"}}</button>
</form>
<form method="post" class="tw-inline-block" id="ai-translate-all-form">
{{.CsrfTokenHtml}}
<input type="hidden" name="action" value="ai_translate_all">
<button class="ui purple tiny button" type="submit">{{svg "octicon-copilot" 14}} {{ctx.Locale.Tr "repo.settings.pages.ai_translate_all"}}</button>
</form>
{{end}}
<form method="post" class="tw-inline-block" id="delete-translation-form" style="display:none;">
{{.CsrfTokenHtml}}
<input type="hidden" name="action" value="delete_translation">
<input type="hidden" name="target_lang" id="delete-translate-lang" value="">
<button class="ui red tiny button" type="submit">{{svg "octicon-trash" 14}} {{ctx.Locale.Tr "repo.settings.pages.delete_translation"}}</button>
</form>
</div>
<div id="ai-translate-loading" class="tw-hidden tw-py-4">
<div class="ui active centered inline loader"></div>
<p class="tw-text-center tw-mt-2" style="color:var(--color-text-light);">{{ctx.Locale.Tr "repo.settings.pages.ai_translating"}}</p>
</div>
{{/* Language panels (one per non-default language, only one visible at a time) */}}
{{range .Config.I18n.Languages}}
{{if ne . $.Config.I18n.DefaultLang}}
<div class="ui segment">
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
<h5 class="tw-m-0">{{index $.LanguageNames .}} ({{.}})</h5>
<div>
{{if $.AIEnabled}}
<form method="post" class="tw-inline-block">
{{$.CsrfTokenHtml}}
<input type="hidden" name="action" value="ai_translate">
<input type="hidden" name="target_lang" value="{{.}}">
<button class="ui purple tiny button">{{ctx.Locale.Tr "repo.settings.pages.ai_translate"}}</button>
</form>
{{end}}
{{if index $.TranslationMap .}}
<form method="post" class="tw-inline-block">
{{$.CsrfTokenHtml}}
<input type="hidden" name="action" value="delete_translation">
<input type="hidden" name="target_lang" value="{{.}}">
<button class="ui red tiny button">{{ctx.Locale.Tr "repo.settings.pages.delete_translation"}}</button>
</form>
{{end}}
</div>
</div>
<div class="lang-panel" id="lang-panel-{{.}}" style="display:none;">
<form class="ui form" method="post">
{{$.CsrfTokenHtml}}
<input type="hidden" name="action" value="save_translation">
@@ -76,6 +89,25 @@
{{$trans := index $.TranslationMap .}}
{{/* Brand */}}
{{if or $.Config.Brand.Name $.Config.Brand.Tagline}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_brand"}}</h5>
{{if $.Config.Brand.Name}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_brand_name"}}</label>
<input name="trans_brand_name" value="{{if $trans}}{{$trans.BrandName}}{{end}}" placeholder="{{$.Config.Brand.Name}}">
</div>
{{end}}
{{if $.Config.Brand.Tagline}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_brand_tagline"}}</label>
<input name="trans_brand_tagline" value="{{if $trans}}{{$trans.BrandTagline}}{{end}}" placeholder="{{$.Config.Brand.Tagline}}">
</div>
{{end}}
{{end}}
{{/* Hero */}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_hero"}}</h5>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_headline"}}</label>
<input name="trans_headline" value="{{if $trans}}{{$trans.Headline}}{{end}}" placeholder="{{$.Config.Hero.Headline}}">
@@ -84,14 +116,110 @@
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_subheadline"}}</label>
<input name="trans_subheadline" value="{{if $trans}}{{$trans.Subheadline}}{{end}}" placeholder="{{$.Config.Hero.Subheadline}}">
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_primary_cta"}}</label>
<input name="trans_primary_cta" value="{{if $trans}}{{$trans.PrimaryCTA}}{{end}}" placeholder="{{$.Config.Hero.PrimaryCTA.Label}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_secondary_cta"}}</label>
<input name="trans_secondary_cta" value="{{if $trans}}{{$trans.SecondaryCTA}}{{end}}" placeholder="{{$.Config.Hero.SecondaryCTA.Label}}">
</div>
</div>
{{/* Stats */}}
{{if $.Config.Stats}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_stats"}}</h5>
{{range $i, $s := $.Config.Stats}}
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_stat_value"}} — <em>{{$s.Value}}</em></label>
<input name="trans_stat_{{$i}}_value" value="{{if $trans}}{{index $trans.StatsValues $i}}{{end}}" placeholder="{{$s.Value}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_stat_label"}} — <em>{{$s.Label}}</em></label>
<input name="trans_stat_{{$i}}_label" value="{{if $trans}}{{index $trans.StatsLabels $i}}{{end}}" placeholder="{{$s.Label}}">
</div>
</div>
{{end}}
{{end}}
{{/* Value Props */}}
{{if $.Config.ValueProps}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_value_props"}}</h5>
{{range $i, $vp := $.Config.ValueProps}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_primary_cta"}}</label>
<input name="trans_primary_cta" value="{{if $trans}}{{$trans.PrimaryCTA}}{{end}}" placeholder="{{$.Config.Hero.PrimaryCTA.Label}}">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_title"}} — <em>{{$vp.Title}}</em></label>
<input name="trans_vp_{{$i}}_title" value="{{if $trans}}{{index $trans.ValuePropTitles $i}}{{end}}" placeholder="{{$vp.Title}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_secondary_cta"}}</label>
<input name="trans_secondary_cta" value="{{if $trans}}{{$trans.SecondaryCTA}}{{end}}" placeholder="{{$.Config.Hero.SecondaryCTA.Label}}">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_description"}}</label>
<input name="trans_vp_{{$i}}_desc" value="{{if $trans}}{{index $trans.ValuePropDescs $i}}{{end}}" placeholder="{{$vp.Description}}">
</div>
{{end}}
{{end}}
{{/* Features */}}
{{if $.Config.Features}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_features"}}</h5>
{{range $i, $f := $.Config.Features}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_title"}} — <em>{{$f.Title}}</em></label>
<input name="trans_feat_{{$i}}_title" value="{{if $trans}}{{index $trans.FeatureTitles $i}}{{end}}" placeholder="{{$f.Title}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_description"}}</label>
<input name="trans_feat_{{$i}}_desc" value="{{if $trans}}{{index $trans.FeatureDescs $i}}{{end}}" placeholder="{{$f.Description}}">
</div>
{{end}}
{{end}}
{{/* Testimonials */}}
{{if $.Config.SocialProof.Testimonials}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_testimonials"}}</h5>
{{range $i, $t := $.Config.SocialProof.Testimonials}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_quote"}} — <em>{{$t.Author}}</em></label>
<textarea name="trans_test_{{$i}}_quote" rows="2" placeholder="{{$t.Quote}}">{{if $trans}}{{index $trans.TestimonialQuotes $i}}{{end}}</textarea>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_role"}}</label>
<input name="trans_test_{{$i}}_role" value="{{if $trans}}{{index $trans.TestimonialRoles $i}}{{end}}" placeholder="{{$t.Role}}">
</div>
{{end}}
{{end}}
{{/* Pricing */}}
{{if or $.Config.Pricing.Headline $.Config.Pricing.Plans}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_pricing"}}</h5>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_pricing_headline"}}</label>
<input name="trans_pricing_headline" value="{{if $trans}}{{$trans.PricingHeadline}}{{end}}" placeholder="{{$.Config.Pricing.Headline}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_pricing_subheadline"}}</label>
<input name="trans_pricing_subheadline" value="{{if $trans}}{{$trans.PricingSubheadline}}{{end}}" placeholder="{{$.Config.Pricing.Subheadline}}">
</div>
{{range $i, $p := $.Config.Pricing.Plans}}
<div class="three fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_plan_name"}} — <em>{{$p.Name}}</em></label>
<input name="trans_plan_{{$i}}_name" value="{{if $trans}}{{index $trans.PlanNames $i}}{{end}}" placeholder="{{$p.Name}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_plan_period"}}</label>
<input name="trans_plan_{{$i}}_period" value="{{if $trans}}{{index $trans.PlanPeriods $i}}{{end}}" placeholder="{{$p.Period}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_plan_cta"}}</label>
<input name="trans_plan_{{$i}}_cta" value="{{if $trans}}{{index $trans.PlanCTAs $i}}{{end}}" placeholder="{{$p.CTA}}">
</div>
</div>
{{end}}
{{end}}
{{/* CTA Section */}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_cta"}}</h5>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_cta_headline"}}</label>
<input name="trans_cta_headline" value="{{if $trans}}{{$trans.CTAHeadline}}{{end}}" placeholder="{{$.Config.CTASection.Headline}}">
@@ -105,6 +233,128 @@
<input name="trans_cta_button" value="{{if $trans}}{{$trans.CTAButton}}{{end}}" placeholder="{{$.Config.CTASection.Button.Label}}">
</div>
{{/* Blog */}}
{{if $.Config.Blog.Enabled}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_blog"}}</h5>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_blog_headline"}}</label>
<input name="trans_blog_headline" value="{{if $trans}}{{$trans.BlogHeadline}}{{end}}" placeholder="{{if $.Config.Blog.Headline}}{{$.Config.Blog.Headline}}{{else}}Latest Posts{{end}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_blog_subheadline"}}</label>
<input name="trans_blog_subheadline" value="{{if $trans}}{{$trans.BlogSubheadline}}{{end}}" placeholder="{{$.Config.Blog.Subheadline}}">
</div>
{{if $.Config.Blog.CTAButton.Label}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_blog_cta"}}</label>
<input name="trans_blog_cta" value="{{if $trans}}{{$trans.BlogCTAButton}}{{end}}" placeholder="{{$.Config.Blog.CTAButton.Label}}">
</div>
{{end}}
{{end}}
{{/* Gallery */}}
{{if $.Config.Gallery.Enabled}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_gallery"}}</h5>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_gallery_headline"}}</label>
<input name="trans_gallery_headline" value="{{if $trans}}{{$trans.GalleryHeadline}}{{end}}" placeholder="{{if $.Config.Gallery.Headline}}{{$.Config.Gallery.Headline}}{{else}}Gallery{{end}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_gallery_subheadline"}}</label>
<input name="trans_gallery_subheadline" value="{{if $trans}}{{$trans.GallerySubheadline}}{{end}}" placeholder="{{$.Config.Gallery.Subheadline}}">
</div>
{{end}}
{{/* Comparison */}}
{{if $.Config.Comparison.Enabled}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_comparison"}}</h5>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_comparison_headline"}}</label>
<input name="trans_comparison_headline" value="{{if $trans}}{{$trans.ComparisonHeadline}}{{end}}" placeholder="{{if $.Config.Comparison.Headline}}{{$.Config.Comparison.Headline}}{{else}}How We Compare{{end}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_comparison_subheadline"}}</label>
<input name="trans_comparison_subheadline" value="{{if $trans}}{{$trans.ComparisonSubheadline}}{{end}}" placeholder="{{$.Config.Comparison.Subheadline}}">
</div>
{{end}}
{{/* Footer */}}
{{if or $.Config.Footer.Copyright $.Config.Footer.Links}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_footer"}}</h5>
{{if $.Config.Footer.Copyright}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_footer_copyright"}}</label>
<input name="trans_footer_copyright" value="{{if $trans}}{{$trans.FooterCopyright}}{{end}}" placeholder="{{$.Config.Footer.Copyright}}">
</div>
{{end}}
{{range $i, $link := $.Config.Footer.Links}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_footer_link"}} — <em>{{$link.Label}}</em></label>
<input name="trans_footer_link_{{$i}}" value="{{if $trans}}{{index $trans.FooterLinkLabels $i}}{{end}}" placeholder="{{$link.Label}}">
</div>
{{end}}
{{end}}
{{/* SEO */}}
{{if or $.Config.SEO.Title $.Config.SEO.Description}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_seo"}}</h5>
{{if $.Config.SEO.Title}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_seo_title"}}</label>
<input name="trans_seo_title" value="{{if $trans}}{{$trans.SEOTitle}}{{end}}" placeholder="{{$.Config.SEO.Title}}">
</div>
{{end}}
{{if $.Config.SEO.Description}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_seo_description"}}</label>
<textarea name="trans_seo_description" rows="2" placeholder="{{$.Config.SEO.Description}}">{{if $trans}}{{$trans.SEODescription}}{{end}}</textarea>
</div>
{{end}}
{{end}}
{{/* Navigation Labels */}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_navigation"}}</h5>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{$.Config.Navigation.LabelValueProps}}</em></label>
<input name="trans_nav_label_value_props" value="{{if $trans}}{{$trans.NavLabelValueProps}}{{end}}" placeholder="{{$.Config.Navigation.LabelValueProps}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{$.Config.Navigation.LabelFeatures}}</em></label>
<input name="trans_nav_label_features" value="{{if $trans}}{{$trans.NavLabelFeatures}}{{end}}" placeholder="{{$.Config.Navigation.LabelFeatures}}">
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{$.Config.Navigation.LabelBlog}}</em></label>
<input name="trans_nav_label_blog" value="{{if $trans}}{{$trans.NavLabelBlog}}{{end}}" placeholder="{{$.Config.Navigation.LabelBlog}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{$.Config.Navigation.LabelGallery}}</em></label>
<input name="trans_nav_label_gallery" value="{{if $trans}}{{$trans.NavLabelGallery}}{{end}}" placeholder="{{$.Config.Navigation.LabelGallery}}">
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{$.Config.Navigation.LabelPricing}}</em></label>
<input name="trans_nav_label_pricing" value="{{if $trans}}{{$trans.NavLabelPricing}}{{end}}" placeholder="{{$.Config.Navigation.LabelPricing}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{$.Config.Navigation.LabelCompare}}</em></label>
<input name="trans_nav_label_compare" value="{{if $trans}}{{$trans.NavLabelCompare}}{{end}}" placeholder="{{$.Config.Navigation.LabelCompare}}">
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{if $.Config.Navigation.LabelDocs}}{{$.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</em></label>
<input name="trans_nav_label_docs" value="{{if $trans}}{{$trans.NavLabelDocs}}{{end}}" placeholder="{{if $.Config.Navigation.LabelDocs}}{{$.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{if $.Config.Navigation.LabelReleases}}{{$.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</em></label>
<input name="trans_nav_label_releases" value="{{if $trans}}{{$trans.NavLabelReleases}}{{end}}" placeholder="{{if $.Config.Navigation.LabelReleases}}{{$.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}">
</div>
</div>
<div class="field">
<button class="ui primary tiny button">{{ctx.Locale.Tr "repo.settings.pages.save_translation"}}</button>
</div>
@@ -115,4 +365,54 @@
</div>
{{end}}
</div>
<script>
(function() {
var selector = document.getElementById('lang-selector');
if (!selector) return;
var aiForm = document.getElementById('ai-translate-form');
var aiLangInput = document.getElementById('ai-translate-lang');
var deleteForm = document.getElementById('delete-translation-form');
var deleteLangInput = document.getElementById('delete-translate-lang');
function showPanel() {
var lang = selector.value;
document.querySelectorAll('.lang-panel').forEach(function(p) { p.style.display = 'none'; });
var panel = document.getElementById('lang-panel-' + lang);
if (panel) panel.style.display = '';
// Update AI translate target
if (aiLangInput) aiLangInput.value = lang;
// Update delete button target + visibility
if (deleteLangInput) deleteLangInput.value = lang;
if (deleteForm) {
// Show delete button only if this language has a translation
var hasTranslation = selector.options[selector.selectedIndex].textContent.indexOf('\u2713') !== -1;
deleteForm.style.display = hasTranslation ? '' : 'none';
}
}
selector.addEventListener('change', showPanel);
// Show first language on load
showPanel();
// Loading spinners for AI translate buttons
function showTranslateLoading(form) {
if (!form) return;
form.addEventListener('submit', function() {
var btn = form.querySelector('button[type="submit"]');
btn.classList.add('loading', 'disabled');
btn.disabled = true;
// Hide all lang panels and action bar buttons, show loader
document.querySelectorAll('.lang-panel').forEach(function(p) { p.style.display = 'none'; });
var loading = document.getElementById('ai-translate-loading');
if (loading) loading.classList.remove('tw-hidden');
});
}
showTranslateLoading(document.getElementById('ai-translate-form'));
showTranslateLoading(document.getElementById('ai-translate-all-form'));
})();
</script>
{{template "repo/settings/layout_footer" .}}

View File

@@ -1,34 +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>
</div>
{{end}}

View File

@@ -97,7 +97,7 @@
<input name="og_image" value="{{.Config.SEO.OGImage}}" placeholder="https://example.com/og-image.png">
</div>
<div id="og-image-preview" {{if not .Config.SEO.UseMediaKitOG}}style="display:none"{{end}}>
<img src="{{.Repository.Link}}/social-preview" style="max-width:400px;border-radius:8px;border:1px solid #ddd;margin-top:8px;">
<img id="og-preview-img" src="{{.Repository.Link}}/social-preview?v={{.Repository.UpdatedUnix}}" style="max-width:400px;border-radius:8px;border:1px solid #ddd;margin-top:8px;">
</div>
</div>
@@ -170,6 +170,10 @@
document.getElementById('use-media-kit-og').addEventListener('change', function() {
document.getElementById('og-image-manual').style.display = this.checked ? 'none' : '';
document.getElementById('og-image-preview').style.display = this.checked ? '' : 'none';
if (this.checked) {
const img = document.getElementById('og-preview-img');
img.src = img.src.replace(/[?&]v=\d+/, '') + '?v=' + Date.now();
}
});
function updateColorPreview(type) {