feat(seo): per-page OG/Twitter/JSON-LD metadata (#202)
Closes #187. Adds canonical links, descriptions, Open Graph and Twitter card metadata, plus Article JSON-LD for weekly and topic pages so Morbo's #190 distribution launch can rely on rich social previews. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Juan Manuel Servera committed
May 25, 2026 at 22:56 UTC
e30d7fded57b11401cef4fb906731fd6c51bc6a0
3 files changed
+53
-10
hugo.toml
+2
@@ -21,6 +21,8 @@ rssLimit = 20
21
22
[params]
23
ga_measurement_id = ""
24
+ description = 'SquadScope turns GitHub activity into readable weekly, monthly, and yearly trend reports for busy developers and tech leads.'
25
+ default_social_image = '/images/squadscope-social-card.png'
26
defaultTheme = 'auto'
27
disableSpecial1stPost = true
28
ShowReadingTime = true
layouts/partials/head.html
+1
-10
@@ -7,8 +7,7 @@
7
<meta name="robots" content="noindex, nofollow">
8
{{- end }}
9
10
-{{- /* Title */}}
11
-<title>{{ if .IsHome }}{{ else }}{{ if .Title }}{{ .Title }} | {{ end }}{{ end }}{{ site.Title }}</title>
10
+{{- partial "seo.html" . -}}
11
12
{{- /* Meta */}}
13
{{- if .IsHome }}
@@ -18,11 +17,6 @@
17
{{- range $i, $e := .Params.keywords }}{{ if $i }}, {{ end }}{{ $e }}{{ end }} {{- else }}
18
{{- range $i, $e := .Params.tags }}{{ if $i }}, {{ end }}{{ $e }}{{ end }} {{- end -}}">
19
{{- end }}
21
-<meta name="description" content="{{- with .Description }}{{ . }}{{- else }}{{- if or .IsPage .IsSection}}
22
- {{- .Summary | default (printf "%s - %s" .Title site.Title) }}{{- else }}
23
- {{- with site.Params.description }}{{ . }}{{- end }}{{- end }}{{- end -}}">
24
-<meta name="author" content="{{ (partial "author.html" . ) }}">
25
-<link rel="canonical" href="{{ if .Params.canonicalURL -}} {{ trim .Params.canonicalURL " " }} {{- else -}} {{ .Permalink }} {{- end }}">
20
{{- if site.Params.analytics.google.SiteVerificationTag }}
21
<meta name="google-site-verification" content="{{ site.Params.analytics.google.SiteVerificationTag }}">
22
{{- end }}
@@ -197,7 +191,4 @@
191
{{- /* Misc */}}
192
{{- if hugo.IsProduction | or (eq site.Params.env "production") }}
193
{{- partial "google_analytics.html" . }}
200
-{{- partial "templates/opengraph.html" . }}
201
-{{- partial "templates/twitter_cards.html" . }}
202
-{{- partial "templates/schema_json.html" . }}
194
{{- end -}}
layouts/partials/seo.html
new
+50
@@ -0,0 +1,50 @@
1
+{{- $title := cond .IsHome .Site.Title (printf "%s | %s" .Title .Site.Title) -}}
2
+{{- $siteDescription := .Site.Params.description | default "SquadScope turns GitHub activity into weekly tech trend analysis for developers and tech leads." -}}
3
+{{- $description := .Description | default .Summary | default $siteDescription | plainify | htmlUnescape | chomp -}}
4
+{{- $description = $description | truncate 155 -}}
5
+{{- $defaultImage := .Site.Params.default_social_image | default "/images/squadscope-social-card.png" -}}
6
+{{- /* Placeholder social card path: Calculon will provide the 1200×630 image asset separately. */ -}}
7
+{{- $imageParam := .Params.featured_image | default $defaultImage -}}
8
+{{- $image := $imageParam -}}
9
+{{- if not (or (hasPrefix $imageParam "http://") (hasPrefix $imageParam "https://")) -}}
10
+{{- $image = (strings.TrimPrefix "/" $imageParam) | absURL -}}
11
+{{- end -}}
12
+{{- $publisherLogo := $defaultImage -}}
13
+{{- if not (or (hasPrefix $publisherLogo "http://") (hasPrefix $publisherLogo "https://")) -}}
14
+{{- $publisherLogo = (strings.TrimPrefix "/" $publisherLogo) | absURL -}}
15
+{{- end -}}
16
+{{- $isArticle := or (and .IsPage (eq .Section "weekly")) (eq .Type "topics") -}}
17
+{{- $type := cond $isArticle "article" "website" -}}
18
+
19
+<title>{{ $title }}</title>
20
+<meta name="description" content="{{ $description }}">
21
+<meta name="author" content="{{ .Site.Params.author | default "SquadScope" }}">
22
+<link rel="canonical" href="{{ .Permalink }}">
23
+
24
+<meta property="og:title" content="{{ $title }}">
25
+<meta property="og:description" content="{{ $description }}">
26
+<meta property="og:image" content="{{ $image }}">
27
+<meta property="og:url" content="{{ .Permalink }}">
28
+<meta property="og:type" content="{{ $type }}">
29
+<meta property="og:site_name" content="{{ .Site.Title }}">
30
+
31
+<meta name="twitter:card" content="summary_large_image">
32
+<meta name="twitter:title" content="{{ $title }}">
33
+<meta name="twitter:description" content="{{ $description }}">
34
+<meta name="twitter:image" content="{{ $image }}">
35
+
36
+{{- if $isArticle }}
37
+{{- $publisher := dict "@type" "Organization" "name" "SquadScope" "logo" (dict "@type" "ImageObject" "url" $publisherLogo) -}}
38
+{{- $schema := dict
39
+ "@context" "https://schema.org"
40
+ "@type" "Article"
41
+ "headline" .Title
42
+ "description" $description
43
+ "image" $image
44
+ "url" .Permalink
45
+ "datePublished" (.Date.Format "2006-01-02T15:04:05Z07:00")
46
+ "author" (dict "@type" "Organization" "name" "SquadScope")
47
+ "publisher" $publisher
48
+-}}
49
+<script type="application/ld+json">{{ $schema | jsonify | safeJS }}</script>
50
+{{- end }}