diff --git a/config.yaml b/config.yaml
index 65bb6f4..e469c20 100644
--- a/config.yaml
+++ b/config.yaml
@@ -16,6 +16,8 @@ languages:
en:
weight: 1
label: English
+ params:
+ description: "Nicolas Massé's website: articles, talks and publications about Kubernetes, OpenShift, API Management, Single Sign-On and homelab."
menu:
main:
- name: "Articles"
@@ -42,6 +44,8 @@ languages:
fr:
weight: 2
label: Français
+ params:
+ description: "Le site de Nicolas Massé : articles, conférences et publications sur Kubernetes, OpenShift, l'API Management, le Single Sign-On et le homelab."
menu:
main:
- name: "Articles"
@@ -70,6 +74,12 @@ languages:
# has not been translated (french files take precedence over english files).
module:
mounts:
+ # Default assets mount, plus the images of the static directory so that
+ # they can be processed by Hugo (social images).
+ - source: assets
+ target: assets
+ - source: static/images
+ target: assets/images
- source: content/english
target: content
sites:
@@ -99,6 +109,10 @@ taxonomies:
params:
enablePlausible: false
+ # Used in the structured data (JSON-LD)
+ author:
+ name: "Nicolas Massé"
+ url: "https://www.linkedin.com/in/nicolasmasse/"
images:
- /images/avatar-2022-12.webp
diff --git a/layouts/_partials/head.html b/layouts/_partials/head.html
index adb4bf1..0e56a8e 100644
--- a/layouts/_partials/head.html
+++ b/layouts/_partials/head.html
@@ -30,19 +30,7 @@
-- Ensure the tag is never empty since it is used
-- in the SEO score.
-->
-{{- if .IsHome }}
-{{- with .Site.Params.description }}
-
-{{- else }}
-
-{{- end }}
-{{- else }}
-{{- with .Params.description }}
-
-{{- else }}
-
-{{- end }}
-{{- end }}
+
{{ with .OutputFormats.Get "rss" -}}
@@ -115,30 +103,32 @@
-- Structured Data can be tested using the Google Search Console.
-- https://search.google.com/test/rich-results
-->
+{{- $structuredData := dict -}}
+{{- $description := partialCached "page-description.html" . .Permalink -}}
{{- if .IsHome -}}
- {{- $.Store.Set "structured_data" (dict "@context" "http://schema.org" "@type" "WebSite" "name" .Site.Title "url" .Site.BaseURL) -}}
- {{- with .Site.Params.subtitle -}}
- {{- $.Store.SetInMap "structured_data" "description" . -}}
+ {{- $structuredData = dict "@context" "https://schema.org" "@type" "WebSite" "name" .Site.Title "url" .Permalink "description" $description "inLanguage" .Language.Lang -}}
+{{- else if and .IsPage (in (slice "blog" "speaking" "writing") .Section) -}}
+ {{- $author := dict "@type" "Person" "name" site.Params.author.name "url" site.Params.author.url -}}
+ {{- $structuredData = dict "@context" "https://schema.org" "@type" (cond (eq .Section "blog") "BlogPosting" "Article") "headline" .Title "description" $description "author" $author "articleSection" .Section -}}
+ {{- with (partialCached "social-image.html" . .Permalink).image -}}
+ {{- $structuredData = merge $structuredData (dict "image" (slice .Permalink)) -}}
{{- end -}}
-{{- else if and .IsPage (eq .Section "blog") -}}
- {{- $.Store.Set "structured_data" (dict "@context" "http://schema.org" "@type" "BlogPosting" "headline" .Title "image" (slice) "articleSection" .Section) -}}
- {{- if and (eq .Lang "fr") (.Store.Get "en_article_in_fr_site") -}}
- {{- $.Store.SetInMap "structured_data" "inLanguage" "en" -}}
+ {{- if .Store.Get "en_article_in_fr_site" -}}
+ {{- $structuredData = merge $structuredData (dict "inLanguage" "en") -}}
{{- range first 1 .Translations -}}
- {{- $.Store.SetInMap "structured_data" "mainEntityOfPage" (dict "@type" "WebPage" "@id" .Permalink) -}}
+ {{- $structuredData = merge $structuredData (dict "mainEntityOfPage" (dict "@type" "WebPage" "@id" .Permalink)) -}}
{{- end -}}
{{- else -}}
- {{- $.Store.SetInMap "structured_data" "inLanguage" .Language.Lang -}}
- {{- $.Store.SetInMap "structured_data" "mainEntityOfPage" (dict "@type" "WebPage" "@id" .Permalink) -}}
+ {{- $structuredData = merge $structuredData (dict "inLanguage" .Language.Lang "mainEntityOfPage" (dict "@type" "WebPage" "@id" .Permalink)) -}}
{{- end -}}
{{- if not .PublishDate.IsZero -}}
- {{- $.Store.SetInMap "structured_data" "datePublished" .PublishDate -}}
+ {{- $structuredData = merge $structuredData (dict "datePublished" .PublishDate) -}}
{{- end -}}
{{- if and (not .Lastmod.IsZero) (ne .Lastmod .PublishDate) -}}
- {{- $.Store.SetInMap "structured_data" "dateModified" .Lastmod -}}
+ {{- $structuredData = merge $structuredData (dict "dateModified" .Lastmod) -}}
{{- end -}}
{{- end -}}
-{{- with $.Store.Get "structured_data" }}
+{{- with $structuredData }}
diff --git a/layouts/_partials/opengraph.html b/layouts/_partials/opengraph.html
new file mode 100644
index 0000000..2edec20
--- /dev/null
+++ b/layouts/_partials/opengraph.html
@@ -0,0 +1,82 @@
+{{- /*
+ -- Based on the Hugo embedded opengraph.html partial (v0.167.0).
+ --
+ -- Changes: the description comes from page-description.html and the image
+ -- from social-image.html (JPEG, 1200 px wide max).
+ */ -}}
+
+
+{{- with or site.Title site.Params.title | plainify }}
+
+{{- end }}
+
+{{- with or .Title site.Title site.Params.title | plainify }}
+
+{{- end }}
+
+{{- with partialCached "page-description.html" . .Permalink }}
+
+{{- end }}
+
+{{- with or .Params.locale site.Language.Locale }}
+
+{{- end }}
+
+{{- if .IsPage }}
+
+ {{- with .Section }}
+
+ {{- end }}
+ {{- $ISO8601 := "2006-01-02T15:04:05-07:00" }}
+ {{- with .PublishDate }}
+
+ {{- end }}
+ {{- with .Lastmod }}
+
+ {{- end }}
+ {{- range .GetTerms "tags" | first 6 }}
+
+ {{- end }}
+{{- else }}
+
+{{- end }}
+
+{{- with (partialCached "social-image.html" . .Permalink).image }}
+
+
+
+
+
+{{- end }}
+
+{{- with .Params.audio }}
+ {{- range . | first 6 }}
+
+ {{- end }}
+{{- end }}
+
+{{- with .Params.videos }}
+ {{- range . | first 6 }}
+
+ {{- end }}
+{{- end }}
+
+{{- range .GetTerms "series" }}
+ {{- range .Pages | first 7 }}
+ {{- if ne $ . }}
+
+ {{- end }}
+ {{- end }}
+{{- end }}
+
+{{- with site.Params.social }}
+ {{- if reflect.IsMap . }}
+ {{- with .facebook_app_id }}
+
+ {{- else }}
+ {{- with .facebook_admin }}
+
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end }}
diff --git a/layouts/_partials/page-description.html b/layouts/_partials/page-description.html
new file mode 100644
index 0000000..c73c176
--- /dev/null
+++ b/layouts/_partials/page-description.html
@@ -0,0 +1,28 @@
+{{- /*
+ -- Returns the description of a page (plain text), used by the meta
+ -- description, Open Graph, Twitter cards and structured data.
+ --
+ -- In order of preference:
+ -- 1. the "description" front matter parameter
+ -- 2. the description of the site, for the home page
+ -- 3. "Articles related to " for the taxonomy term pages
+ -- 4. the summary of the page, truncated to ~160 characters
+ -- 5. the description of the site
+ -- 6. the title of the page
+ */ -}}
+{{- $description := "" -}}
+{{- if .Description -}}
+ {{- $description = .Description -}}
+{{- else if and .IsHome site.Params.description -}}
+ {{- $description = site.Params.description -}}
+{{- else if eq .Kind "term" -}}
+ {{- $description = printf "%s %s" (i18n "articles_related_to" | strings.TrimSpace) .Title -}}
+{{- else -}}
+ {{- $summary := .Summary | plainify | htmlUnescape | replaceRE `\s+` " " | strings.TrimSpace -}}
+ {{- with $summary -}}
+ {{- $description = truncate 160 . -}}
+ {{- else -}}
+ {{- $description = or site.Params.description .Title -}}
+ {{- end -}}
+{{- end -}}
+{{- return ($description | plainify | htmlUnescape | strings.TrimSpace) -}}
diff --git a/layouts/_partials/social-image.html b/layouts/_partials/social-image.html
new file mode 100644
index 0000000..945bb1f
--- /dev/null
+++ b/layouts/_partials/social-image.html
@@ -0,0 +1,76 @@
+{{- /*
+ -- Returns the image used when the page is shared on social networks
+ -- (Open Graph, Twitter cards) and in the structured data, as a dict:
+ -- - image: the processed image resource
+ -- - isDefault: true if the default image of the site is used
+ --
+ -- The image is chosen in this order:
+ -- 1. the first image of the "images" front matter parameter
+ -- 2. a page resource named *feature*, *cover* or *thumbnail*
+ -- 3. the first image displayed in the page content
+ -- 4. the first image of the "images" site parameter
+ --
+ -- The image is converted to JPEG (WebP is not supported everywhere) and
+ -- resized to 1200 px wide max, without cropping: the social networks crop
+ -- it to their own aspect ratio.
+ */ -}}
+{{- $formats := slice "jpeg" "png" "webp" "gif" "bmp" "tiff" -}}
+{{- $images := slice -}}
+{{- range .Resources.ByType "image" -}}
+ {{- if in $formats .MediaType.SubType -}}
+ {{- $images = $images | append . -}}
+ {{- end -}}
+{{- end -}}
+
+{{- $source := false -}}
+{{- $isDefault := false -}}
+
+{{- /* 1. The "images" front matter parameter */ -}}
+{{- range $name := .Params.images -}}
+ {{- if not $source -}}
+ {{- range $images -}}
+ {{- if and (not $source) (eq .Name $name) -}}{{- $source = . -}}{{- end -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- /* 2. A page resource named *feature*, *cover* or *thumbnail* */ -}}
+{{- if not $source -}}
+ {{- range $images -}}
+ {{- if and (not $source) (findRE `(?i)(feature|cover|thumbnail)` .Name) -}}{{- $source = . -}}{{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- /* 3. The first image displayed in the page content */ -}}
+{{- if and (not $source) $images -}}
+ {{- range findRESubmatch `
]*\ssrc="([^"]+)"` .Content -}}
+ {{- $src := index . 1 -}}
+ {{- if not $source -}}
+ {{- range $images -}}
+ {{- if and (not $source) (or (eq .RelPermalink $src) (eq .Permalink $src)) -}}{{- $source = . -}}{{- end -}}
+ {{- end -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- /* 4. The default image of the site */ -}}
+{{- if not $source -}}
+ {{- with index site.Params.images 0 -}}
+ {{- with resources.Get (strings.TrimPrefix "/" .) -}}
+ {{- $source = . -}}
+ {{- $isDefault = true -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- $result := dict -}}
+{{- with $source -}}
+ {{- $image := . -}}
+ {{- if gt .Width 1200 -}}
+ {{- $image = .Resize "1200x jpg q85" -}}
+ {{- else -}}
+ {{- $image = .Process "jpg q85" -}}
+ {{- end -}}
+ {{- $result = dict "image" $image "isDefault" $isDefault -}}
+{{- end -}}
+{{- return $result -}}
diff --git a/layouts/_partials/twitter_cards.html b/layouts/_partials/twitter_cards.html
new file mode 100644
index 0000000..85e68df
--- /dev/null
+++ b/layouts/_partials/twitter_cards.html
@@ -0,0 +1,36 @@
+{{- /*
+ -- Based on the Hugo embedded twitter_cards.html partial (v0.167.0).
+ --
+ -- Changes: the description comes from page-description.html and the image
+ -- from social-image.html (JPEG, 1200 px wide max).
+ */ -}}
+{{- /* The default image (avatar) is better displayed as a small card. */}}
+{{- $social := partialCached "social-image.html" . .Permalink }}
+{{- with $social.image }}
+
+
+
+{{- else }}
+
+{{- end }}
+
+{{- with or .Title site.Title site.Params.title | plainify }}
+
+{{- end }}
+
+{{- with partialCached "page-description.html" . .Permalink }}
+
+{{- end }}
+
+{{- $twitterSite := "" }}
+{{- with site.Params.social }}
+ {{- if reflect.IsMap . }}
+ {{- with .twitter }}
+ {{- $content := . }}
+ {{- if not (strings.HasPrefix . "@") }}
+ {{- $content = printf "@%v" . }}
+ {{- end }}
+
+ {{- end }}
+ {{- end }}
+{{- end }}