feat(pages): serve social preview on custom domains
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m1s
Build and Release / Unit Tests (push) Successful in 4m35s
Build and Release / Lint (push) Failing after 5m58s
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
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m1s
Build and Release / Unit Tests (push) Successful in 4m35s
Build and Release / Lint (push) Failing after 5m58s
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
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.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
{{end}}
|
||||
{{end}}
|
||||
<meta property="og:type" content="object">
|
||||
<meta property="og:image" content="{{AppUrl}}{{.Repository.OwnerName}}/{{.Repository.Name}}/social-preview">
|
||||
<meta property="og:image" content="{{AppUrl}}{{.Repository.OwnerName}}/{{.Repository.Name}}/social-preview.png">
|
||||
{{if or (eq .Repository.SocialCardTheme "solid") (eq .Repository.SocialCardTheme "image")}}
|
||||
<meta property="og:image:width" content="1080">
|
||||
<meta property="og:image:height" content="1350">
|
||||
@@ -71,7 +71,7 @@
|
||||
{{end}}
|
||||
<meta property="og:image:type" content="image/png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image" content="{{AppUrl}}{{.Repository.OwnerName}}/{{.Repository.Name}}/social-preview">
|
||||
<meta name="twitter:image" content="{{AppUrl}}{{.Repository.OwnerName}}/{{.Repository.Name}}/social-preview.png">
|
||||
{{else}}
|
||||
<meta property="og:title" content="{{AppName}}">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
<meta name="twitter:image" content="{{.BlogPost.FeaturedImage.DownloadURL}}">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
{{else if .Config.SEO.UseMediaKitOG}}
|
||||
<meta property="og:image" content="{{AppUrl}}{{.Repository.OwnerName}}/{{.Repository.Name}}/social-preview">
|
||||
<meta name="twitter:image" content="{{AppUrl}}{{.Repository.OwnerName}}/{{.Repository.Name}}/social-preview">
|
||||
<meta property="og:image" content="{{.SocialPreviewURL}}">
|
||||
<meta name="twitter:image" content="{{.SocialPreviewURL}}">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
{{else if .Config.SEO.OGImage}}
|
||||
<meta property="og:image" content="{{.Config.SEO.OGImage}}">
|
||||
@@ -40,8 +40,8 @@
|
||||
<meta name="twitter:title" content="{{if .Config.SEO.Title}}{{.Config.SEO.Title}}{{else if .Config.Hero.Headline}}{{.Config.Hero.Headline}}{{else}}{{.Repository.Name}}{{end}}">
|
||||
<meta name="twitter:description" content="{{if .Config.SEO.Description}}{{.Config.SEO.Description}}{{else if .Config.Hero.Subheadline}}{{.Config.Hero.Subheadline}}{{else}}{{.Repository.Description}}{{end}}">
|
||||
{{if .Config.SEO.UseMediaKitOG}}
|
||||
<meta property="og:image" content="{{AppUrl}}{{.Repository.OwnerName}}/{{.Repository.Name}}/social-preview">
|
||||
<meta name="twitter:image" content="{{AppUrl}}{{.Repository.OwnerName}}/{{.Repository.Name}}/social-preview">
|
||||
<meta property="og:image" content="{{.SocialPreviewURL}}">
|
||||
<meta name="twitter:image" content="{{.SocialPreviewURL}}">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
{{else if .Config.SEO.OGImage}}
|
||||
<meta property="og:image" content="{{.Config.SEO.OGImage}}">
|
||||
|
||||
Reference in New Issue
Block a user