mirror of
https://github.com/thomiceli/opengist.git
synced 2026-08-07 07:14:49 +00:00
feat: add Open Graph & Twitter Card meta tags for gist social sharing (#756)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.26, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.26, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, macOS-latest) (push) Has been cancelled
Go CI / Build (1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, windows-latest) (push) Has been cancelled
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.26, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.26, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, macOS-latest) (push) Has been cancelled
Go CI / Build (1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, windows-latest) (push) Has been cancelled
Adds Open Graph and Twitter Card meta tags to gist pages so that pasting an Opengist URL into Discord, Slack, IRC and similar clients produces a rich preview card (like gist.github.com does). - templates/base/seo_meta.html (new): reusable partial rendering og:* and twitter:* tags, gated on a loaded gist. - templates/base/base_header.html: include the partial in <head>. - internal/web/server/renderer.go: add ogText helper that collapses whitespace and truncates text for clean meta values. - internal/web/handlers/gist/gist_test.go: TestGistSocialMetaTags. - CHANGELOG.md: entry. Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
+2
-1
@@ -3,7 +3,8 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Seed a user/admin account at install time from the CLI (`admin create-user`), enabling automated provisioning with tools like Ansible (#1)
|
||||
- Seed a user/admin account at install time from the CLI (`admin create-user`), enabling automated provisioning with tools like Ansible (#750)
|
||||
- Open Graph and Twitter Card meta tags on gist pages for rich social sharing previews in Discord/Slack/IRC clients (#756)
|
||||
|
||||
## [1.13.1](https://github.com/thomiceli/opengist/compare/v1.13.0...v1.13.1) - 2026-06-10
|
||||
See here how to [update](https://opengist.io/docs/update) Opengist.
|
||||
|
||||
@@ -100,6 +100,49 @@ func TestGistIndex(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGistSocialMetaTags(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
|
||||
t.Run("RendersOpenGraphAndTwitterCards", func(t *testing.T) {
|
||||
_, gist, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier, nil, 200)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
html := string(body)
|
||||
|
||||
// Open Graph tags
|
||||
assert.Contains(t, html, `property="og:title"`)
|
||||
assert.Contains(t, html, `property="og:description"`)
|
||||
assert.Contains(t, html, `property="og:type" content="article"`)
|
||||
assert.Contains(t, html, `property="og:url"`)
|
||||
assert.Contains(t, html, `property="og:site_name" content="Opengist"`)
|
||||
assert.Contains(t, html, `property="og:image"`)
|
||||
|
||||
// Twitter Card tags
|
||||
assert.Contains(t, html, `name="twitter:card" content="summary"`)
|
||||
assert.Contains(t, html, `name="twitter:title"`)
|
||||
assert.Contains(t, html, `name="twitter:description"`)
|
||||
assert.Contains(t, html, `name="twitter:image"`)
|
||||
|
||||
// The card reflects the gist owner/title and points at the gist URL
|
||||
assert.Contains(t, html, gist.Title)
|
||||
assert.Contains(t, html, username+"/"+identifier)
|
||||
})
|
||||
|
||||
t.Run("UnlistedGistAlsoHasMetaTags", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "1")
|
||||
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier, nil, 200)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), `property="og:title"`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPreview(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
@@ -188,6 +188,18 @@ func (s *Server) setFuncMap() {
|
||||
}
|
||||
return str
|
||||
},
|
||||
// ogText collapses all whitespace (newlines, tabs, multiple spaces) into
|
||||
// single spaces and truncates to maxRunes runes, appending an ellipsis when
|
||||
// truncated. Used to build clean values for Open Graph / Twitter meta tags
|
||||
// from multi-line gist descriptions or code previews.
|
||||
"ogText": func(s string, maxRunes int) string {
|
||||
collapsed := strings.Join(strings.Fields(s), " ")
|
||||
runes := []rune(collapsed)
|
||||
if len(runes) <= maxRunes {
|
||||
return collapsed
|
||||
}
|
||||
return string(runes[:maxRunes]) + "\u2026"
|
||||
},
|
||||
"hexToRgb": func(hex string) string {
|
||||
h, _ := strconv.ParseUint(strings.TrimPrefix(hex, "#"), 16, 32)
|
||||
return fmt.Sprintf("%d, %d, %d,", (h>>16)&0xFF, (h>>8)&0xFF, h&0xFF)
|
||||
|
||||
Vendored
+2
@@ -12,6 +12,8 @@
|
||||
{{ if .canonicalUrl }}
|
||||
<link rel="canonical" href="{{ .canonicalUrl }}" />
|
||||
{{ end }}
|
||||
|
||||
{{ template "seo_meta" . }}
|
||||
|
||||
<script nonce="{{ .cspNonce }}">
|
||||
window.opengist_base_url = "{{ $.c.ExternalUrl }}";
|
||||
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
{{/* Open Graph / Twitter Card meta tags for social sharing previews.
|
||||
Rendered on any page that has a loaded gist (e.g. gist view, revisions) so
|
||||
that pasting a gist URL into Discord / IRC / Slack shows a rich preview,
|
||||
like gist.github.com does. */}}
|
||||
{{ define "seo_meta" }}
|
||||
{{ if .gist }}
|
||||
{{ $siteName := "Opengist" }}{{ if $.c.CustomName }}{{ $siteName = $.c.CustomName }}{{ end }}
|
||||
{{ $description := "" }}{{ if .gist.Description }}{{ $description = .gist.Description }}{{ else }}{{ $description = .gist.Preview }}{{ end }}
|
||||
{{ $ogDescription := ogText $description 200 }}
|
||||
{{ $ogTitle := ogText (printf "%s/%s" .gist.User.Username .gist.Title) 100 }}
|
||||
|
||||
<meta property="og:title" content="{{ $ogTitle }}" />
|
||||
<meta property="og:description" content="{{ $ogDescription }}" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="{{ .canonicalUrl }}" />
|
||||
<meta property="og:site_name" content="{{ $siteName }}" />
|
||||
{{ if shouldGenerateAvatar .gist.User .DisableGravatar }}
|
||||
<meta property="og:image" content="{{ asset "img/favicon-32.png" }}" />
|
||||
{{ else }}
|
||||
<meta property="og:image" content="{{ avatarUrl .gist.User .DisableGravatar }}" />
|
||||
{{ end }}
|
||||
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content="{{ $ogTitle }}" />
|
||||
<meta name="twitter:description" content="{{ $ogDescription }}" />
|
||||
{{ if shouldGenerateAvatar .gist.User .DisableGravatar }}
|
||||
<meta name="twitter:image" content="{{ asset "img/favicon-32.png" }}" />
|
||||
{{ else }}
|
||||
<meta name="twitter:image" content="{{ avatarUrl .gist.User .DisableGravatar }}" />
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user