From e8b630397109ad50d00da33cb49d97f96ee3058d Mon Sep 17 00:00:00 2001 From: logikonline Date: Mon, 16 Mar 2026 02:46:41 -0400 Subject: [PATCH] feat(pages): serve social preview on custom domains Add /social-preview.png endpoint for custom domain/subdomain landing pages. Generates social card using same logic as repo social preview but accessible on custom domains. Adds .png extension to social preview URLs for better compatibility with social media crawlers. Updates og:image and twitter:image meta tags to use domain-relative URLs on custom domains. Includes ETag caching with 30min max-age. --- routers/web/pages/pages.go | 96 +++++++++++++++++++++++++++++- routers/web/web.go | 1 + templates/base/head_opengraph.tmpl | 4 +- templates/pages/base_head.tmpl | 8 +-- 4 files changed, 102 insertions(+), 7 deletions(-) diff --git a/routers/web/pages/pages.go b/routers/web/pages/pages.go index 28452e4cf1..e8fc1ae743 100644 --- a/routers/web/pages/pages.go +++ b/routers/web/pages/pages.go @@ -5,10 +5,14 @@ package pages import ( "crypto/rand" + "crypto/sha256" "encoding/hex" "errors" "fmt" "html/template" + "image" + _ "image/jpeg" + _ "image/png" "io" "math/big" "net/http" @@ -28,6 +32,8 @@ import ( "code.gitcaddy.com/server/v3/modules/log" "code.gitcaddy.com/server/v3/modules/markup/markdown" pages_module "code.gitcaddy.com/server/v3/modules/pages" + "code.gitcaddy.com/server/v3/modules/socialcard" + "code.gitcaddy.com/server/v3/modules/storage" "code.gitcaddy.com/server/v3/modules/templates" "code.gitcaddy.com/server/v3/services/context" pages_service "code.gitcaddy.com/server/v3/services/pages" @@ -70,6 +76,12 @@ func ServeLandingPage(ctx *context.Context) { return } + // Handle social preview image + if requestPath == "/social-preview.png" || requestPath == "/social-preview" { + serveSocialPreview(ctx, repo) + return + } + // Handle asset requests (gallery images, custom assets) // Uses /pages/assets/ to avoid conflict with Gitea's static /assets/ route if assetPath, found := strings.CutPrefix(requestPath, "/pages/assets/"); found && assetPath != "" { @@ -164,7 +176,13 @@ func setupLandingPageContext(ctx *context.Context, repo *repo_model.Repository, if ctx.Req.TLS == nil && strings.HasPrefix(ctx.Req.Host, "localhost") { scheme = "http" } - ctx.Data["PageURL"] = scheme + "://" + ctx.Req.Host + ctx.Req.URL.RequestURI() + pageBase := scheme + "://" + ctx.Req.Host + ctx.Data["PageURL"] = pageBase + ctx.Req.URL.RequestURI() + ctx.Data["SocialPreviewURL"] = pageBase + "/" + repo.OwnerName + "/" + repo.Name + "/social-preview.png" + // On custom domains, use a relative path so the URL resolves to the custom domain + if ctx.Repo == nil || ctx.Repo.Repository == nil { + ctx.Data["SocialPreviewURL"] = pageBase + "/social-preview.png" + } ctx.Data["NumStars"] = repo.NumStars ctx.Data["NumForks"] = repo.NumForks ctx.Data["Year"] = time.Now().Year() @@ -551,6 +569,82 @@ func serveCustomDomainAsset(ctx *context.Context, repo *repo_model.Repository, a serveRepoFileAsset(ctx, commit, assetPath) } +// 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) { + if err := repo.LoadAttributes(ctx); err != nil { + ctx.ServerError("LoadAttributes", err) + return + } + + title := repo.DisplayTitle + if title == "" { + title = repo.Name + } + + ownerName := repo.OwnerName + if repo.Owner != nil { + ownerName = repo.Owner.DisplayName() + } + repoName := repo.Name + if repo.DisplayTitle != "" { + repoName = repo.DisplayTitle + } + + data := socialcard.CardData{ + Title: title, + Description: repo.Description, + RepoAvatarURL: repo.AvatarLink(ctx), + RepoFullName: ownerName + " / " + repoName, + BrandName: repo.OwnerDisplayName, + SolidColor: repo.SocialCardColor, + UnsplashAuthor: repo.SocialCardUnsplashAuthor, + } + + if repo.PrimaryLanguage != nil { + data.LanguageName = repo.PrimaryLanguage.Language + data.LanguageColor = repo.PrimaryLanguage.Color + } + + themeName := repo.SocialCardTheme + if (themeName == "image" || themeName == "solid") && repo.SocialCardBgImage != "" { + if fr, err := storage.RepoAvatars.Open(repo.SocialCardBgImage); err == nil { + defer fr.(io.ReadCloser).Close() + if img, _, err := image.Decode(fr.(io.Reader)); err == nil { + data.BgImage = img + } + } + } + + theme := socialcard.GetTheme(themeName) + renderer, err := socialcard.GlobalRenderer() + if err != nil { + ctx.ServerError("socialcard.GlobalRenderer", err) + return + } + + pngData, err := renderer.RenderCard(data, theme) + if err != nil { + ctx.ServerError("RenderCard", err) + return + } + + hash := sha256.Sum256(pngData) + etag := fmt.Sprintf(`"%x"`, hash[:16]) + + ctx.Resp.Header().Set("Content-Type", "image/png") + ctx.Resp.Header().Set("Cache-Control", "public, max-age=1800") + ctx.Resp.Header().Set("ETag", etag) + + if ctx.Req.Header.Get("If-None-Match") == etag { + ctx.Resp.WriteHeader(http.StatusNotModified) + return + } + + ctx.Resp.WriteHeader(http.StatusOK) + _, _ = ctx.Resp.Write(pngData) +} + // ServePageAsset serves static assets for the landing page func ServePageAsset(ctx *context.Context) { repo, _, err := getRepoFromRequest(ctx) diff --git a/routers/web/web.go b/routers/web/web.go index da200ad492..60a4ad5c10 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1398,6 +1398,7 @@ func registerWebRoutes(m *web.Router) { m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup) m.Get("/{username}/{reponame}/social-preview", optSignIn, context.RepoAssignment, repo.SocialPreview) + m.Get("/{username}/{reponame}/social-preview.png", optSignIn, context.RepoAssignment, repo.SocialPreview) // Subscribe page (requires sign-in, no code access check) m.Get("/{username}/{reponame}/subscribe", reqSignIn, context.RepoAssignment, repo.Subscribe) diff --git a/templates/base/head_opengraph.tmpl b/templates/base/head_opengraph.tmpl index 8afc7203b0..adb17276e1 100644 --- a/templates/base/head_opengraph.tmpl +++ b/templates/base/head_opengraph.tmpl @@ -61,7 +61,7 @@ {{end}} {{end}} - + {{if or (eq .Repository.SocialCardTheme "solid") (eq .Repository.SocialCardTheme "image")}} @@ -71,7 +71,7 @@ {{end}} - + {{else}} diff --git a/templates/pages/base_head.tmpl b/templates/pages/base_head.tmpl index 1db6b573f9..71b7ca2f4c 100644 --- a/templates/pages/base_head.tmpl +++ b/templates/pages/base_head.tmpl @@ -22,8 +22,8 @@ {{else if .Config.SEO.UseMediaKitOG}} - - + + {{else if .Config.SEO.OGImage}} @@ -40,8 +40,8 @@ {{if .Config.SEO.UseMediaKitOG}} - - + + {{else if .Config.SEO.OGImage}}