gitea/routers/web/pages/pages.go
logikonline b816ee4eec feat: add Phases 3-5 enhancements (org profiles, pages, wiki v2 API)
Phase 3: Organization Public Profile Page
- Pinned repositories with groups
- Public members display with roles
- API endpoints for pinned repos and groups

Phase 4: Gitea Pages Foundation
- Landing page templates (simple, docs, product, portfolio)
- Custom domain support with verification
- YAML configuration parser (.gitea/landing.yaml)
- Repository settings UI for pages

Phase 5: Enhanced Wiki System with V2 API
- Full CRUD operations via v2 API
- Full-text search with WikiIndex table
- Link graph visualization
- Wiki health metrics (orphaned, dead links, outdated)
- Designed for external AI plugin integration
- Developer guide for .NET integration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 15:14:27 -05:00

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"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/renderhelper"
"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"
}