// 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" }