Some checks failed
Build and Release / Build Binaries (amd64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, linux) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, windows) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, linux) (push) Blocked by required conditions
Build and Release / Build Docker Image (push) Blocked by required conditions
Build and Release / Create Release (push) Blocked by required conditions
Build and Release / Lint and Test (push) Has been cancelled
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
424 lines
10 KiB
Go
424 lines
10 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package pages
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models/renderhelper"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/modules/git"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/markup/markdown"
|
|
pages_module "code.gitea.io/gitea/modules/pages"
|
|
"code.gitea.io/gitea/modules/templates"
|
|
"code.gitea.io/gitea/services/context"
|
|
pages_service "code.gitea.io/gitea/services/pages"
|
|
)
|
|
|
|
const (
|
|
tplPagesSimple templates.TplName = "pages/simple"
|
|
tplPagesDocumentation templates.TplName = "pages/documentation"
|
|
tplPagesProduct templates.TplName = "pages/product"
|
|
tplPagesPortfolio templates.TplName = "pages/portfolio"
|
|
)
|
|
|
|
// ServeLandingPage serves the landing page for a repository
|
|
func ServeLandingPage(ctx *context.Context) {
|
|
// Get the repository from subdomain or custom domain
|
|
repo, config, err := getRepoFromRequest(ctx)
|
|
if err != nil {
|
|
log.Error("Failed to get repo from pages request: %v", err)
|
|
ctx.NotFound(err)
|
|
return
|
|
}
|
|
|
|
if repo == nil || config == nil || !config.Enabled {
|
|
ctx.NotFound(fmt.Errorf("pages not configured"))
|
|
return
|
|
}
|
|
|
|
// Check for redirect
|
|
requestPath := ctx.Req.URL.Path
|
|
if config.Advanced.Redirects != nil {
|
|
if redirect, ok := config.Advanced.Redirects[requestPath]; ok {
|
|
ctx.Redirect(redirect)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Render the landing page
|
|
if err := renderLandingPage(ctx, repo, config); err != nil {
|
|
log.Error("Failed to render landing page: %v", err)
|
|
ctx.ServerError("Failed to render landing page", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// getRepoFromRequest extracts the repository from the pages request
|
|
func getRepoFromRequest(ctx *context.Context) (*repo_model.Repository, *pages_module.LandingConfig, error) {
|
|
host := ctx.Req.Host
|
|
|
|
// Check for custom domain first
|
|
repo, err := pages_service.GetRepoByPagesDomain(ctx, host)
|
|
if err == nil && repo != nil {
|
|
config, err := pages_service.GetPagesConfig(ctx, repo)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return repo, config, nil
|
|
}
|
|
|
|
// Parse subdomain: {repo}.{owner}.pages.{domain}
|
|
// This is a simplified implementation
|
|
parts := strings.Split(host, ".")
|
|
if len(parts) < 4 {
|
|
return nil, nil, fmt.Errorf("invalid pages subdomain")
|
|
}
|
|
|
|
repoName := parts[0]
|
|
ownerName := parts[1]
|
|
|
|
repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
config, err := pages_service.GetPagesConfig(ctx, repo)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return repo, config, nil
|
|
}
|
|
|
|
// renderLandingPage renders the landing page based on the template
|
|
func renderLandingPage(ctx *context.Context, repo *repo_model.Repository, config *pages_module.LandingConfig) error {
|
|
// Set up context data
|
|
ctx.Data["Repository"] = repo
|
|
ctx.Data["Config"] = config
|
|
ctx.Data["Title"] = getPageTitle(repo, config)
|
|
ctx.Data["PageIsPagesLanding"] = true
|
|
|
|
// Load README content
|
|
readme, err := loadReadmeContent(ctx, repo, config)
|
|
if err != nil {
|
|
log.Warn("Failed to load README: %v", err)
|
|
}
|
|
ctx.Data["ReadmeContent"] = readme
|
|
|
|
// Load repo stats
|
|
ctx.Data["NumStars"] = repo.NumStars
|
|
ctx.Data["NumForks"] = repo.NumForks
|
|
|
|
// Select template based on config
|
|
tpl := selectTemplate(config.Template)
|
|
|
|
ctx.HTML(http.StatusOK, tpl)
|
|
return nil
|
|
}
|
|
|
|
// getPageTitle returns the page title
|
|
func getPageTitle(repo *repo_model.Repository, config *pages_module.LandingConfig) string {
|
|
if config.SEO.Title != "" {
|
|
return config.SEO.Title
|
|
}
|
|
if config.Hero.Title != "" {
|
|
return config.Hero.Title
|
|
}
|
|
return repo.Name
|
|
}
|
|
|
|
// loadReadmeContent loads and renders the README content
|
|
func loadReadmeContent(ctx *context.Context, repo *repo_model.Repository, config *pages_module.LandingConfig) (template.HTML, error) {
|
|
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer gitRepo.Close()
|
|
|
|
branch := repo.DefaultBranch
|
|
if branch == "" {
|
|
branch = "main"
|
|
}
|
|
|
|
commit, err := gitRepo.GetBranchCommit(branch)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Find README file
|
|
readmePath := findReadmePath(commit, config)
|
|
if readmePath == "" {
|
|
return "", fmt.Errorf("README not found")
|
|
}
|
|
|
|
entry, err := commit.GetTreeEntryByPath(readmePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
reader, err := entry.Blob().DataAsync()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer reader.Close()
|
|
|
|
content := make([]byte, entry.Blob().Size())
|
|
_, err = reader.Read(content)
|
|
if err != nil && err.Error() != "EOF" {
|
|
return "", err
|
|
}
|
|
|
|
// Render markdown using renderhelper
|
|
rctx := renderhelper.NewRenderContextRepoFile(ctx, repo)
|
|
rendered, err := markdown.RenderString(rctx, string(content))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return rendered, nil
|
|
}
|
|
|
|
// findReadmePath finds the README file path
|
|
func findReadmePath(commit *git.Commit, config *pages_module.LandingConfig) string {
|
|
// Check config for custom readme location
|
|
for _, section := range config.Sections {
|
|
if section.Type == "readme" && section.File != "" {
|
|
return section.File
|
|
}
|
|
}
|
|
|
|
// Default README locations
|
|
readmePaths := []string{
|
|
"README.md",
|
|
"readme.md",
|
|
"Readme.md",
|
|
"README.markdown",
|
|
"README.txt",
|
|
"README",
|
|
}
|
|
|
|
for _, p := range readmePaths {
|
|
if _, err := commit.GetTreeEntryByPath(p); err == nil {
|
|
return p
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// selectTemplate selects the template based on configuration
|
|
func selectTemplate(templateName string) templates.TplName {
|
|
switch templateName {
|
|
case "documentation":
|
|
return tplPagesDocumentation
|
|
case "product":
|
|
return tplPagesProduct
|
|
case "portfolio":
|
|
return tplPagesPortfolio
|
|
default:
|
|
return tplPagesSimple
|
|
}
|
|
}
|
|
|
|
// ServePageAsset serves static assets for the landing page
|
|
func ServePageAsset(ctx *context.Context) {
|
|
repo, _, err := getRepoFromRequest(ctx)
|
|
if err != nil {
|
|
ctx.NotFound(err)
|
|
return
|
|
}
|
|
|
|
// Get the asset path from URL
|
|
assetPath := strings.TrimPrefix(ctx.Req.URL.Path, "/assets/")
|
|
if assetPath == "" {
|
|
ctx.NotFound(fmt.Errorf("asset not found"))
|
|
return
|
|
}
|
|
|
|
// Load asset from repository
|
|
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
|
|
}
|
|
|
|
// Try assets folder first
|
|
fullPath := path.Join("assets", assetPath)
|
|
entry, err := commit.GetTreeEntryByPath(fullPath)
|
|
if err != nil {
|
|
// Try .gitea/assets
|
|
fullPath = path.Join(".gitea", "assets", assetPath)
|
|
entry, err = commit.GetTreeEntryByPath(fullPath)
|
|
if err != nil {
|
|
ctx.NotFound(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
reader, err := entry.Blob().DataAsync()
|
|
if err != nil {
|
|
ctx.ServerError("Failed to read asset", err)
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
|
|
// Set content type based on extension
|
|
ext := path.Ext(assetPath)
|
|
contentType := getContentType(ext)
|
|
ctx.Resp.Header().Set("Content-Type", contentType)
|
|
ctx.Resp.Header().Set("Cache-Control", "public, max-age=3600")
|
|
|
|
// Stream content
|
|
content := make([]byte, entry.Blob().Size())
|
|
_, err = reader.Read(content)
|
|
if err != nil && err.Error() != "EOF" {
|
|
ctx.ServerError("Failed to read asset", err)
|
|
return
|
|
}
|
|
|
|
ctx.Resp.Write(content)
|
|
}
|
|
|
|
// ServeRepoLandingPage serves the landing page for a repository via URL path
|
|
func ServeRepoLandingPage(ctx *context.Context) {
|
|
repo := ctx.Repo.Repository
|
|
if repo == nil {
|
|
ctx.NotFound(fmt.Errorf("repository not found"))
|
|
return
|
|
}
|
|
|
|
config, err := pages_service.GetPagesConfig(ctx, repo)
|
|
if err != nil {
|
|
log.Error("Failed to get pages config: %v", err)
|
|
ctx.NotFound(err)
|
|
return
|
|
}
|
|
|
|
if config == nil || !config.Enabled {
|
|
ctx.NotFound(fmt.Errorf("pages not enabled for this repository"))
|
|
return
|
|
}
|
|
|
|
// Render the landing page
|
|
if err := renderLandingPage(ctx, repo, config); err != nil {
|
|
log.Error("Failed to render landing page: %v", err)
|
|
ctx.ServerError("Failed to render landing page", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// ServeRepoPageAsset serves static assets for the landing page via URL path
|
|
func ServeRepoPageAsset(ctx *context.Context) {
|
|
repo := ctx.Repo.Repository
|
|
if repo == nil {
|
|
ctx.NotFound(fmt.Errorf("repository not found"))
|
|
return
|
|
}
|
|
|
|
// Get the asset path from URL
|
|
assetPath := ctx.PathParam("*")
|
|
if assetPath == "" {
|
|
ctx.NotFound(fmt.Errorf("asset not found"))
|
|
return
|
|
}
|
|
|
|
// Load asset from repository
|
|
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
|
|
}
|
|
|
|
// Try assets folder first
|
|
fullPath := path.Join("assets", assetPath)
|
|
entry, err := commit.GetTreeEntryByPath(fullPath)
|
|
if err != nil {
|
|
// Try .gitea/assets
|
|
fullPath = path.Join(".gitea", "assets", assetPath)
|
|
entry, err = commit.GetTreeEntryByPath(fullPath)
|
|
if err != nil {
|
|
ctx.NotFound(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
reader, err := entry.Blob().DataAsync()
|
|
if err != nil {
|
|
ctx.ServerError("Failed to read asset", err)
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
|
|
// Set content type based on extension
|
|
ext := path.Ext(assetPath)
|
|
contentType := getContentType(ext)
|
|
ctx.Resp.Header().Set("Content-Type", contentType)
|
|
ctx.Resp.Header().Set("Cache-Control", "public, max-age=3600")
|
|
|
|
// Stream content
|
|
content := make([]byte, entry.Blob().Size())
|
|
_, err = reader.Read(content)
|
|
if err != nil && err.Error() != "EOF" {
|
|
ctx.ServerError("Failed to read asset", err)
|
|
return
|
|
}
|
|
|
|
ctx.Resp.Write(content)
|
|
}
|
|
|
|
// getContentType returns the content type for a file extension
|
|
func getContentType(ext string) string {
|
|
types := map[string]string{
|
|
".html": "text/html",
|
|
".css": "text/css",
|
|
".js": "application/javascript",
|
|
".json": "application/json",
|
|
".png": "image/png",
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".gif": "image/gif",
|
|
".svg": "image/svg+xml",
|
|
".ico": "image/x-icon",
|
|
".woff": "font/woff",
|
|
".woff2": "font/woff2",
|
|
".ttf": "font/ttf",
|
|
".eot": "application/vnd.ms-fontobject",
|
|
}
|
|
|
|
if ct, ok := types[strings.ToLower(ext)]; ok {
|
|
return ct
|
|
}
|
|
return "application/octet-stream"
|
|
}
|