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

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:
Alexander Fortin
2026-07-11 05:56:06 +03:00
committed by GitHub
parent 64e9c9aa53
commit c3f86616f5
5 changed files with 91 additions and 1 deletions
+2 -1
View File
@@ -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.
+43
View File
@@ -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)
+12
View File
@@ -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)
+2
View File
@@ -12,6 +12,8 @@
{{ if .canonicalUrl }}
<link rel="canonical" href="{{ .canonicalUrl }}" />
{{ end }}
{{ template "seo_meta" . }}
<script nonce="{{ .cspNonce }}">
window.opengist_base_url = "{{ $.c.ExternalUrl }}";
+32
View File
@@ -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 }}