diff --git a/routers/controllers/share.go b/routers/controllers/share.go
index 3aa549ca..eda524c2 100644
--- a/routers/controllers/share.go
+++ b/routers/controllers/share.go
@@ -1,11 +1,13 @@
package controllers
import (
+ "net/http"
+ "strings"
+
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
"github.com/cloudreve/Cloudreve/v4/service/share"
"github.com/gin-gonic/gin"
- "net/http"
)
// CreateShare 创建分享
@@ -74,5 +76,48 @@ func DeleteShare(c *gin.Context) {
func ShareRedirect(c *gin.Context) {
service := ParametersFromContext[*share.ShortLinkRedirectService](c, share.ShortLinkRedirectParamCtx{})
+
+ // Check if the request is from a social media bot
+ if isSocialMediaBot(c.GetHeader("User-Agent")) {
+ html, err := service.RenderOGPage(c)
+ if err == nil {
+ c.Header("Content-Type", "text/html; charset=utf-8")
+ c.String(http.StatusOK, html)
+ return
+ }
+ // Fall back to redirect on error
+ }
+
c.Redirect(http.StatusFound, service.RedirectTo(c))
}
+
+// isSocialMediaBot checks if the User-Agent belongs to a social media crawler
+func isSocialMediaBot(ua string) bool {
+ bots := []string{
+ "facebookexternalhit",
+ "twitterbot",
+ "linkedinbot",
+ "whatsapp",
+ "slackbot",
+ "discordbot",
+ "telegrambot",
+ "applebot",
+ "pinterest",
+ "googlebot",
+ "bingbot",
+ "yandexbot",
+ "duckduckbot",
+ "baiduspider",
+ "sogou",
+ "exabot",
+ "facebot",
+ "ia_archiver",
+ }
+ ua = strings.ToLower(ua)
+ for _, bot := range bots {
+ if strings.Contains(ua, bot) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/service/share/og_template.go b/service/share/og_template.go
new file mode 100644
index 00000000..e55eed08
--- /dev/null
+++ b/service/share/og_template.go
@@ -0,0 +1,77 @@
+package share
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+)
+
+// ShareOGData contains data for rendering Open Graph HTML page
+type ShareOGData struct {
+ SiteName string
+ FileName string
+ FileSize string
+ OwnerName string
+ ShareURL string
+ ThumbnailURL string
+ RedirectURL string
+}
+
+const ogHTMLTemplate = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{.FileName}} - {{.SiteName}}
+
+
+
+
+
+`
+
+var ogTemplate = template.Must(template.New("og").Parse(ogHTMLTemplate))
+
+// RenderOGHTML renders the Open Graph HTML page
+func RenderOGHTML(data *ShareOGData) (string, error) {
+ var buf bytes.Buffer
+ if err := ogTemplate.Execute(&buf, data); err != nil {
+ return "", fmt.Errorf("failed to render OG template: %w", err)
+ }
+ return buf.String(), nil
+}
+
+// FormatFileSize formats file size to human readable format
+func FormatFileSize(size int64) string {
+ const (
+ B = 1
+ KB = 1024 * B
+ MB = 1024 * KB
+ GB = 1024 * MB
+ TB = 1024 * GB
+ )
+
+ switch {
+ case size >= TB:
+ return fmt.Sprintf("%.2f TB", float64(size)/TB)
+ case size >= GB:
+ return fmt.Sprintf("%.2f GB", float64(size)/GB)
+ case size >= MB:
+ return fmt.Sprintf("%.2f MB", float64(size)/MB)
+ case size >= KB:
+ return fmt.Sprintf("%.2f KB", float64(size)/KB)
+ default:
+ return fmt.Sprintf("%d B", size)
+ }
+}
diff --git a/service/share/visit.go b/service/share/visit.go
index c47f10fb..ee69fded 100644
--- a/service/share/visit.go
+++ b/service/share/visit.go
@@ -2,6 +2,7 @@ package share
import (
"context"
+ "net/url"
"strings"
"github.com/cloudreve/Cloudreve/v4/application/dependency"
@@ -48,6 +49,68 @@ func (s *ShortLinkRedirectService) RedirectTo(c *gin.Context) string {
return shareLongUrl.String()
}
+// RenderOGPage renders an Open Graph HTML page for social media previews
+func (s *ShortLinkRedirectService) RenderOGPage(c *gin.Context) (string, error) {
+ dep := dependency.FromContext(c)
+ shareClient := dep.ShareClient()
+ settings := dep.SettingProvider()
+
+ // Load share with user and file info
+ ctx := context.WithValue(c, inventory.LoadShareUser{}, true)
+ ctx = context.WithValue(ctx, inventory.LoadShareFile{}, true)
+ share, err := shareClient.GetByHashID(ctx, s.ID)
+ if err != nil {
+ return "", err
+ }
+
+ // Check if share is valid
+ if err := inventory.IsValidShare(share); err != nil {
+ return "", err
+ }
+
+ // Get site settings
+ siteBasic := settings.SiteBasic(c)
+ pwa := settings.PWA(c)
+ base := settings.SiteURL(c)
+
+ // Build share URL
+ shareURL := routes.MasterShareUrl(base, s.ID, "")
+
+ // Build thumbnail URL - use PWA large icon (PNG), fallback to medium icon
+ thumbnailURL := pwa.LargeIcon
+ if thumbnailURL == "" {
+ thumbnailURL = pwa.MediumIcon
+ }
+ if thumbnailURL != "" && !isAbsoluteURL(thumbnailURL) {
+ thumbnailURL = base.ResolveReference(&url.URL{Path: thumbnailURL}).String()
+ }
+
+ // Get file info
+ fileName := share.Edges.File.Name
+ fileSize := FormatFileSize(share.Edges.File.Size)
+
+ // Get owner name (don't expose email for privacy)
+ ownerName := share.Edges.User.Nick
+
+ // Prepare OG data
+ ogData := &ShareOGData{
+ SiteName: siteBasic.Name,
+ FileName: fileName,
+ FileSize: fileSize,
+ OwnerName: ownerName,
+ ShareURL: shareURL.String(),
+ ThumbnailURL: thumbnailURL,
+ RedirectURL: s.RedirectTo(c),
+ }
+
+ return RenderOGHTML(ogData)
+}
+
+// isAbsoluteURL checks if the URL is absolute (starts with http:// or https://)
+func isAbsoluteURL(u string) bool {
+ return strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://")
+}
+
type (
ShareInfoService struct {
Password string `form:"password"`