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 }}