mirror of
https://github.com/jbowdre/runtimeterror.git
synced 2024-11-26 00:42:18 +00:00
Compare commits
17 commits
9d7a7cbff8
...
fbaf8384a0
Author | SHA1 | Date | |
---|---|---|---|
fbaf8384a0 | |||
540c8958a6 | |||
962381dce6 | |||
c3dc2ce2db | |||
203465fea2 | |||
b889920b42 | |||
3f3518f108 | |||
1c92120c7d | |||
ba15df513d | |||
47b528f1f4 | |||
0a92f57a10 | |||
9e515a7c19 | |||
55c8934ebc | |||
02626117fd | |||
6312c163d0 | |||
6780a6b6eb | |||
a2e52c3d56 |
7 changed files with 276 additions and 56 deletions
|
@ -1,17 +1,10 @@
|
||||||
---
|
---
|
||||||
<<<<<<< HEAD
|
|
||||||
title: "Displaying Data from Tempest Weather Station on a Static Site"
|
|
||||||
=======
|
|
||||||
title: "Displaying Data from a Tempest Weather Station on a Static Site"
|
title: "Displaying Data from a Tempest Weather Station on a Static Site"
|
||||||
>>>>>>> main
|
|
||||||
date: "2024-02-11T20:48:49Z"
|
date: "2024-02-11T20:48:49Z"
|
||||||
# lastmod: 2024-02-10
|
# lastmod: 2024-02-10
|
||||||
description: "Using a GitHub Actions workflow to retrieve data from an authenticated API, posting results to a publicly-accessible pastebin, and displaying them on a static web site."
|
description: "Using a GitHub Actions workflow to retrieve data from an authenticated API, posting results to a publicly-accessible pastebin, and displaying them on a static web site."
|
||||||
featured: false
|
featured: false
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
thumbnail: "finished-product.png"
|
thumbnail: "finished-product.png"
|
||||||
>>>>>>> main
|
|
||||||
toc: true
|
toc: true
|
||||||
comments: true
|
comments: true
|
||||||
categories: Backstage
|
categories: Backstage
|
||||||
|
@ -21,11 +14,7 @@ tags:
|
||||||
- meta
|
- meta
|
||||||
- serverless
|
- serverless
|
||||||
---
|
---
|
||||||
<<<<<<< HEAD
|
|
||||||
As I covered briefly [in a recent Scribble](https://scribbles.jbowdre.lol/post/near-realtime-weather-on-profile-lol-ku4yq-zr), I was inspired by the way [Kris's omg.lol page](https://kris.omg.lol/) displays realtime data from his [Weatherflow Tempest weather station](https://shop.weatherflow.com/products/tempest). I thought that was really neat and wanted to do the same on [my omg.lol page](https://jbowdre.lol) with data from my own Tempest, but I wanted to do it without including my station ID or API token directly in the client-side JavaScript.
|
|
||||||
=======
|
|
||||||
As I covered briefly [in a recent Scribble](https://scribbles.jbowdre.lol/post/near-realtime-weather-on-profile-lol-ku4yq-zr), I was inspired by the way [Kris's omg.lol page](https://kris.omg.lol/) displays realtime data from his [Weatherflow Tempest weather station](https://shop.weatherflow.com/products/tempest). I thought that was really neat and wanted to do the same on [my omg.lol page](https://jbowdre.lol) with data from my own Tempest, but I wanted to find a way to do it without needing to include an authenticated API call in the client-side JavaScript.
|
As I covered briefly [in a recent Scribble](https://scribbles.jbowdre.lol/post/near-realtime-weather-on-profile-lol-ku4yq-zr), I was inspired by the way [Kris's omg.lol page](https://kris.omg.lol/) displays realtime data from his [Weatherflow Tempest weather station](https://shop.weatherflow.com/products/tempest). I thought that was really neat and wanted to do the same on [my omg.lol page](https://jbowdre.lol) with data from my own Tempest, but I wanted to find a way to do it without needing to include an authenticated API call in the client-side JavaScript.
|
||||||
>>>>>>> main
|
|
||||||
|
|
||||||
I realized I could use a GitHub Actions workflow to retrieve the data from the authenticated Tempest API, post it somewhere publicly accessible, and then have the client-side code fetch the data from there without needing any authentication. After a few days of tinkering, I came up with a presentation I'm happy with.
|
I realized I could use a GitHub Actions workflow to retrieve the data from the authenticated Tempest API, post it somewhere publicly accessible, and then have the client-side code fetch the data from there without needing any authentication. After a few days of tinkering, I came up with a presentation I'm happy with.
|
||||||
|
|
||||||
|
@ -619,17 +608,10 @@ For testing this, I can manually alter[^alter] the contents of the pastebin to e
|
||||||
![Local test page reflecting "57°f (13.9°c), feels 67°f (19.4°c)" and "no rain today"](local-test-conditional.png)
|
![Local test page reflecting "57°f (13.9°c), feels 67°f (19.4°c)" and "no rain today"](local-test-conditional.png)
|
||||||
|
|
||||||
The final touch will be to change the icons depending on the values of certain fields:
|
The final touch will be to change the icons depending on the values of certain fields:
|
||||||
<<<<<<< HEAD
|
|
||||||
- The conditions icon will vary from <i class="fa-solid fa-sun"></i> to <i class="fa-solid fa-cloud-bolt"></i>, roughly corresponding to the `icon` value from the Tempest API.
|
|
||||||
- The temperature icon will range from <i class="fa-solid fa-thermometer-empty"></i> to <i class="fa-solid fa-thermometer-full"></i> depending on the temperature.
|
|
||||||
- The precipitation icon will be <i class="fa-solid fa-droplet-slash"></i> for no precipitation, and progress through <i class="fa-solid fa-glass-water-droplet"></i> and <i class="fa-solid fa-glass-water"></i> for a bit more rain, and cap out at <i class="fa-solid fa-bucket"></i> for lots of rain[^rain].
|
|
||||||
- The pressure icon should change based on the trend: <i class="fa-solid fa-arrow-trend-up"></i>, <i class="fa-solid fa-arrow-right-long"></i>, and <i class="fa-solid fa-arrow-trend-down"></i>.
|
|
||||||
=======
|
|
||||||
- The conditions icon will vary from <i class='fa-solid fa-cloud-sun'></i> to <i class='fa-solid fa-cloud-showers-heavy'></i>, roughly corresponding to the `icon` value from the Tempest API.
|
- The conditions icon will vary from <i class='fa-solid fa-cloud-sun'></i> to <i class='fa-solid fa-cloud-showers-heavy'></i>, roughly corresponding to the `icon` value from the Tempest API.
|
||||||
- The temperature icon will range from <i class='fa-solid fa-thermometer-empty'></i> to <i class='fa-solid fa-thermometer-full'></i> depending on the temperature.
|
- The temperature icon will range from <i class='fa-solid fa-thermometer-empty'></i> to <i class='fa-solid fa-thermometer-full'></i> depending on the temperature.
|
||||||
- The precipitation icon will be <i class='fa-solid fa-droplet-slash'></i> for no precipitation, and progress through <i class='fa-solid fa-glass-water-droplet'></i> and <i class='fa-solid fa-glass-water'></i> for a bit more rain, and cap out at <i class='fa-solid fa-bucket'></i> for lots of rain[^rain].
|
- The precipitation icon will be <i class='fa-solid fa-droplet-slash'></i> for no precipitation, and progress through <i class='fa-solid fa-glass-water-droplet'></i> and <i class='fa-solid fa-glass-water'></i> for a bit more rain, and cap out at <i class='fa-solid fa-bucket'></i> for lots of rain[^rain].
|
||||||
- The pressure icon should change based on the trend: <i class='fa-solid fa-arrow-trend-up'></i>, <i class='fa-solid fa-arrow-right-long'></i>, and <i class='fa-solid fa-arrow-trend-down'></i>.
|
- The pressure icon should change based on the trend: <i class='fa-solid fa-arrow-trend-up'></i>, <i class='fa-solid fa-arrow-right-long'></i>, and <i class='fa-solid fa-arrow-trend-down'></i>.
|
||||||
>>>>>>> main
|
|
||||||
|
|
||||||
[^rain]: Okay, admittedly this progression isn't perfect, but it's the best I could come up with within the free FA icon set.
|
[^rain]: Okay, admittedly this progression isn't perfect, but it's the best I could come up with within the free FA icon set.
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
258
content/posts/dynamic-opengraph-images-with-hugo/index.md
Normal file
258
content/posts/dynamic-opengraph-images-with-hugo/index.md
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
---
|
||||||
|
title: "Dynamically Generating OpenGraph Images With Hugo"
|
||||||
|
date: "2024-02-19T04:12:27Z"
|
||||||
|
# lastmod: 2024-02-18
|
||||||
|
description: "Using Hugo built-in functions to dynamically generate OpenGraph share images for every post."
|
||||||
|
featured: false
|
||||||
|
toc: true
|
||||||
|
comments: true
|
||||||
|
thumbnail: hugo-logo-wide.png
|
||||||
|
categories: Backstage
|
||||||
|
tags:
|
||||||
|
- hugo
|
||||||
|
- meta
|
||||||
|
- selfhosting
|
||||||
|
---
|
||||||
|
I've lately seen some folks on [social.lol](https://social.lol) posting about their various strategies for automatically generating [Open Graph images](https://ogp.me/) for their [Eleventy](https://11ty.dev) sites. So this weekend I started exploring ways to do that for my [Hugo](https://gohugo.io) site.
|
||||||
|
|
||||||
|
During my search, I came across a few different approaches using external services or additional scripts to run at build time, but I was hoping for a way to do this with Hugo's built-in tooling. I eventually came across a tremendously helpful post from Aaro titled [Generating OpenGraph images with Hugo](https://aarol.dev/posts/hugo-og-image/). This solution was exactly what I was after, as it uses Hugo's [image functions](https://gohugo.io/functions/images/filter/) to dynamically create a share image for each page.
|
||||||
|
|
||||||
|
I ended up borrowing heavily from Aaro's approach while adding a few small variations for my OpenGraph images.
|
||||||
|
- When sharing the home page, the image includes the site description.
|
||||||
|
- When sharing a post, the image includes the post title.
|
||||||
|
- ... but if the post has a thumbnail[^thumbnail] listed in the front matter, that gets overlaid in the corner.
|
||||||
|
|
||||||
|
[^thumbnail]: My current theme doesn't make use of the thumbnails, but a previous theme did so I've got a bunch of posts with thumbnails still assigned. And now I've got a use for them again!
|
||||||
|
|
||||||
|
Here's how I did it.
|
||||||
|
|
||||||
|
### New resources
|
||||||
|
Based on Aaro's suggestions, I used [GIMP](https://www.gimp.org/) to create a 1200x600 image for the base. I'm not a graphic designer[^web] so I kept it simple while trying to match the theme, font, and colors used on the site.
|
||||||
|
|
||||||
|
I had to install the Fira Mono font [Fira Mono `.ttf`](https://github.com/mozilla/Fira/blob/master/ttf/FiraMono-Regular.ttf) to my `~/.fonts/` folder so I could use it in GIMP.
|
||||||
|
|
||||||
|
![Red background with a command prompt displaying "[runtimeterror.dev] $" in white and red font.](og_base.png)
|
||||||
|
|
||||||
|
[^web]: Or a web designer, if I'm being honest.
|
||||||
|
|
||||||
|
That fits with the vibe of the site, and leaves plenty of room for text to be added to the image.
|
||||||
|
|
||||||
|
I also wanted to use that font later for the text overlay, so I stashed both of those resources in my `assets/` folder:
|
||||||
|
|
||||||
|
![File explorer window showing a directory structure with folders such as '.github/workflows', 'archetypes', 'assets' with subfolders 'css', 'js', and files 'FiraMono-Regular.ttf', 'og_base.png' under 'RUNTIMETERROR'.](new_resources.png)
|
||||||
|
|
||||||
|
### OpenGraph partial
|
||||||
|
Hugo uses an [internal template](https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/opengraph.html) for rendering OpenGraph properties by default. I needed to import that as a partial so that I could override its behavior. So I dropped the following in `layouts/partials/opengraph.html` as a starting point:
|
||||||
|
|
||||||
|
```jinja-html
|
||||||
|
// torchlight! {"lineNumbers": true}
|
||||||
|
<meta property="og:title" content="{{ .Title }}" />
|
||||||
|
<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}" />
|
||||||
|
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
|
||||||
|
<meta property="og:url" content="{{ .Permalink }}" />
|
||||||
|
<meta property="og:locale" content="{{ .Lang }}" />
|
||||||
|
|
||||||
|
{{- if .IsPage }}
|
||||||
|
{{- $iso8601 := "2006-01-02T15:04:05-07:00" -}}
|
||||||
|
<meta property="article:section" content="{{ .Section }}" />
|
||||||
|
{{ with .PublishDate }}<meta property="article:published_time" {{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />{{ end }}
|
||||||
|
{{ with .Lastmod }}<meta property="article:modified_time" {{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />{{ end }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- with .Params.audio }}<meta property="og:audio" content="{{ . }}" />{{ end }}
|
||||||
|
{{- with .Params.locale }}<meta property="og:locale" content="{{ . }}" />{{ end }}
|
||||||
|
{{- with .Site.Params.title }}<meta property="og:site_name" content="{{ . }}" />{{ end }}
|
||||||
|
{{- with .Params.videos }}{{- range . }}
|
||||||
|
<meta property="og:video" content="{{ . | absURL }}" />
|
||||||
|
{{ end }}{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
To use this new partial, I added it to my `layouts/partials/head.html`:
|
||||||
|
|
||||||
|
```jinja-html
|
||||||
|
{{ partial "opengraph" . }}
|
||||||
|
```
|
||||||
|
|
||||||
|
which is in turn loaded by `layouts/_defaults/baseof.html`:
|
||||||
|
|
||||||
|
```jinja-html
|
||||||
|
<head>
|
||||||
|
{{- partial "head.html" . -}}
|
||||||
|
</head>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Aaro's OG image generation
|
||||||
|
[Aaro's code](https://aarol.dev/posts/hugo-og-image/) provided the base functionality for what I need:
|
||||||
|
|
||||||
|
```jinja-html
|
||||||
|
{{/* Generate opengraph image */}}
|
||||||
|
{{- if .IsPage -}}
|
||||||
|
{{ $base := resources.Get "og_base.png" }}
|
||||||
|
{{ $boldFont := resources.Get "/Inter-SemiBold.ttf"}}
|
||||||
|
{{ $mediumFont := resources.Get "/Inter-Medium.ttf"}}
|
||||||
|
{{ $img := $base.Filter (images.Text .Site.Title (dict
|
||||||
|
"color" "#ffffff"
|
||||||
|
"size" 52
|
||||||
|
"linespacing" 2
|
||||||
|
"x" 141
|
||||||
|
"y" 117
|
||||||
|
"font" $boldFont
|
||||||
|
))}}
|
||||||
|
{{ $img = $img.Filter (images.Text .Page.Title (dict
|
||||||
|
"color" "#ffffff"
|
||||||
|
"size" 64
|
||||||
|
"linespacing" 2
|
||||||
|
"x" 141
|
||||||
|
"y" 291
|
||||||
|
"font" $mediumFont
|
||||||
|
))}}
|
||||||
|
{{ $img = resources.Copy (path.Join .Page.RelPermalink "og.png") $img }}
|
||||||
|
<meta property="og:image" content="{{$img.Permalink}}">
|
||||||
|
<meta property="og:image:width" content="{{$img.Width}}" />
|
||||||
|
<meta property="og:image:height" content="{{$img.Height}}" />
|
||||||
|
|
||||||
|
<!-- Twitter metadata (used by other websites as well) -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="{{ .Title }}" />
|
||||||
|
<meta name="twitter:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end -}}"/>
|
||||||
|
<meta name="twitter:image" content="{{$img.Permalink}}" />
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
The [`resources.Get`](https://gohugo.io/functions/resources/get/) bits import the image and font resources to make them available to the [`images.Text`](https://gohugo.io/functions/images/text/) functions, which add the site and page title texts to the image using the designated color, size, placement, and font.
|
||||||
|
|
||||||
|
The `resources.Copy` line moves the generated OG image into the post bundle directory and gives it a clean `og.png` name rather than the very-long randomly-generated name it would have by default.
|
||||||
|
|
||||||
|
And then the `<meta ... />` lines insert the generated image into the page's `<head>` block so it can be rendered when the link is shared on sites which support OpenGraph.
|
||||||
|
|
||||||
|
This is a great starting point for what I wanted to accomplish, but made some changes in my `opengraph.html` partial to tailor it to my needs.
|
||||||
|
|
||||||
|
### My tweaks
|
||||||
|
As I mentioned earlier, I wanted to have three slightly-different recipes for baking my OG images: one for the homepage, one for standard posts, and one for posts with an associated thumbnail. They all use the same basic code, though, so I wanted to be sure that my setup didn't repeat itself too much.
|
||||||
|
|
||||||
|
My code starts with fetching my resources up front, and initializing an empty `$text` variable to hold the description or title:
|
||||||
|
|
||||||
|
```jinja-html
|
||||||
|
{{ $img := resources.Get "og_base.png" }}
|
||||||
|
{{ $font := resources.Get "/FiraMono-Regular.ttf" }}
|
||||||
|
{{ $text := "" }}
|
||||||
|
```
|
||||||
|
|
||||||
|
For the site homepage, I set `$text` to hold the site description:
|
||||||
|
|
||||||
|
```jinja-html
|
||||||
|
{{- if .IsHome }}
|
||||||
|
{{ $text = .Site.Params.Description }}
|
||||||
|
{{- end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
On standard post pages, I used the page title instead:
|
||||||
|
|
||||||
|
```jinja-html
|
||||||
|
{{- if .IsPage }}
|
||||||
|
{{ $text = .Page.Title }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the page has a `thumbnail` parameter defined in the front matter, Hugo will use `.Resources.Get` to grab the image.
|
||||||
|
|
||||||
|
```jinja-html
|
||||||
|
{{- with .Params.thumbnail }}
|
||||||
|
{{ $thumbnail := $.Resources.Get . }}
|
||||||
|
```
|
||||||
|
|
||||||
|
{{% notice note "Resources vs resources" %}}
|
||||||
|
The [`resources.Get` function](https://gohugo.io/functions/resources/get/) (little r) I used earlier works on *global* resources, like the image and font stored in the site's `assets/` directory. On the other hand, the [`Resources.Get` method](https://gohugo.io/methods/page/resources/) (big R) is used for loading *page* resources, like the file indicated by the page's `thumbnail` parameter.
|
||||||
|
{{% /notice %}}
|
||||||
|
|
||||||
|
And since I'm calling this method from inside a `with` branch I have to put a `$` in front of the method. Otherwise, the leading `.` would refer directly to the `thumbnail` parameter (which isn't a page and so doesn't have the method available[^scope]).
|
||||||
|
|
||||||
|
[^scope]: Hugo scoping is kind of wild.
|
||||||
|
|
||||||
|
Anyhoo, after the thumbnail is loaded, I use the [`Fit` image processing](https://gohugo.io/content-management/image-processing/#fit) to scale down the thumbnail and then call the [`images.Overlay` function](https://gohugo.io/functions/images/overlay/) to *overlay* it near the top right corner of the `og_base.png` image.
|
||||||
|
|
||||||
|
```jinja-html
|
||||||
|
{{ with $thumbnail }}
|
||||||
|
{{ $img = $img.Filter (images.Overlay (.Process "fit 300x250") 875 38 )}}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then I insert the desired text:
|
||||||
|
|
||||||
|
```jinja-html
|
||||||
|
{{ $img = $img.Filter (images.Text $text (dict
|
||||||
|
"color" "#d8d8d8"
|
||||||
|
"size" 64
|
||||||
|
"linespacing" 2
|
||||||
|
"x" 40
|
||||||
|
"y" 300
|
||||||
|
"font" $font
|
||||||
|
))}}
|
||||||
|
{{ $img = resources.Copy (path.Join $.Page.RelPermalink "og.png") $img }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### All together now
|
||||||
|
|
||||||
|
After merging my code in with the existing `layouts/partials/opengraph.html`, here's what the whole file looks like:
|
||||||
|
|
||||||
|
```jinja-html
|
||||||
|
// torchlight! {"lineNumbers": true}
|
||||||
|
{{ $img := resources.Get "og_base.png" }} <!-- [tl! **:2] -->
|
||||||
|
{{ $font := resources.Get "/FiraMono-Regular.ttf" }}
|
||||||
|
{{ $text := "" }}
|
||||||
|
<meta property="og:title" content="{{ .Title }}" />
|
||||||
|
<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}" />
|
||||||
|
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
|
||||||
|
<meta property="og:url" content="{{ .Permalink }}" />
|
||||||
|
<meta property="og:locale" content="{{ .Lang }}" />
|
||||||
|
{{- if .IsHome }} <!-- [tl! **:2] -->
|
||||||
|
{{ $text = .Site.Params.Description }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- if .IsPage }}
|
||||||
|
{{- $iso8601 := "2006-01-02T15:04:05-07:00" -}}
|
||||||
|
<meta property="article:section" content="{{ .Section }}" />
|
||||||
|
{{ with .PublishDate }}<meta property="article:published_time" {{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />{{ end }}
|
||||||
|
{{ with .Lastmod }}<meta property="article:modified_time" {{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />{{ end }}
|
||||||
|
{{ $text = .Page.Title }} <!-- [tl! ** ] -->
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{- with .Params.thumbnail }} <!-- [tl! **:start] -->
|
||||||
|
{{ $thumbnail := $.Resources.Get . }}
|
||||||
|
{{ with $thumbnail }}
|
||||||
|
{{ $img = $img.Filter (images.Overlay (.Process "fit 300x250") 875 38 )}}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ $img = $img.Filter (images.Text $text (dict
|
||||||
|
"color" "#d8d8d8"
|
||||||
|
"size" 64
|
||||||
|
"linespacing" 2
|
||||||
|
"x" 40
|
||||||
|
"y" 300
|
||||||
|
"font" $font
|
||||||
|
))}}
|
||||||
|
{{ $img = resources.Copy (path.Join $.Page.RelPermalink "og.png") $img }} <!-- [tl! **:end] -->
|
||||||
|
|
||||||
|
<meta property="og:image" content="{{$img.Permalink}}">
|
||||||
|
<meta property="og:image:width" content="{{$img.Width}}" />
|
||||||
|
<meta property="og:image:height" content="{{$img.Height}}" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="{{ .Title }}" />
|
||||||
|
<meta name="twitter:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end -}}"/>
|
||||||
|
<meta name="twitter:image" content="{{$img.Permalink}}" />
|
||||||
|
|
||||||
|
{{- with .Params.audio }}<meta property="og:audio" content="{{ . }}" />{{ end }}
|
||||||
|
{{- with .Site.Params.title }}<meta property="og:site_name" content="{{ . }}" />{{ end }}
|
||||||
|
{{- with .Params.videos }}{{- range . }}
|
||||||
|
<meta property="og:video" content="{{ . | absURL }}" />
|
||||||
|
{{ end }}{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
And it works!
|
||||||
|
![Black background with text "Dynamic Opengraph Images With Hugo", a command prompt "[runtimeterror.dev] $", and colorful hexagon shapes with "HUGO" letters.](og-demo.png)
|
||||||
|
|
||||||
|
I'm sure this could be further optimized by someone who knows what they're doing[^future]. I'd really like to find a better way of positioning the thumbnail overlay to better account for different heights and widths. But for now, I'm pretty happy with how it works, and I enjoyed learning more about Hugo along the way.
|
||||||
|
|
||||||
|
[^future]: Like Future John, perhaps? Past John loves leaving stuff for that guy to figure out.
|
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
BIN
content/posts/dynamic-opengraph-images-with-hugo/og-demo.png
Normal file
BIN
content/posts/dynamic-opengraph-images-with-hugo/og-demo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
BIN
content/posts/dynamic-opengraph-images-with-hugo/og_base.png
Normal file
BIN
content/posts/dynamic-opengraph-images-with-hugo/og_base.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -1,34 +1,31 @@
|
||||||
{{ $base := resources.Get "og_base.png" }}
|
{{ $img := resources.Get "og_base.png" }}
|
||||||
{{ $font := resources.Get "/FiraMono-Regular.ttf" }}
|
{{ $font := resources.Get "/FiraMono-Regular.ttf" }}
|
||||||
|
{{ $text := "" }}
|
||||||
<meta property="og:title" content="{{ .Title }}" />
|
<meta property="og:title" content="{{ .Title }}" />
|
||||||
<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}" />
|
<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}" />
|
||||||
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
|
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
|
||||||
<meta property="og:url" content="{{ .Permalink }}" />
|
<meta property="og:url" content="{{ .Permalink }}" />
|
||||||
<meta property="og:locale" content="{{ .Lang }}" />
|
<meta property="og:locale" content="{{ .Lang }}" />
|
||||||
{{- if .IsHome }}
|
{{- if .IsHome }}
|
||||||
{{ $img := $base.Filter (images.Text .Site.Params.Description (dict
|
{{ $text = .Site.Params.Description }}
|
||||||
"color" "#d8d8d8"
|
|
||||||
"size" 64
|
|
||||||
"linespacing" 2
|
|
||||||
"x" 40
|
|
||||||
"y" 300
|
|
||||||
"font" $font
|
|
||||||
))}}
|
|
||||||
{{ $img = resources.Copy "og.png" $img }}
|
|
||||||
{{ .Scratch.Set "og_image" $img }}
|
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{- if .IsPage }}
|
{{- if .IsPage }}
|
||||||
{{- $iso8601 := "2006-01-02T15:04:05-07:00" -}}
|
{{- $iso8601 := "2006-01-02T15:04:05-07:00" -}}
|
||||||
<meta property="article:section" content="{{ .Section }}" />
|
<meta property="article:section" content="{{ .Section }}" />
|
||||||
{{ with .PublishDate }}<meta property="article:published_time" {{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />{{ end }}
|
{{ with .PublishDate }}<meta property="article:published_time" {{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />{{ end }}
|
||||||
{{ with .Lastmod }}<meta property="article:modified_time" {{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />{{ end }}
|
{{ with .Lastmod }}<meta property="article:modified_time" {{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />{{ end }}
|
||||||
{{ with .Params.thumbnail }}
|
{{ $text = .Page.Title }}
|
||||||
{{/* $thumbPath := (add $.Page.RelPermalink . ) */}}
|
{{ end }}
|
||||||
{{ $thumbnail := $.Resources.Get . }}
|
|
||||||
{{ with $thumbnail }}
|
{{- with .Params.thumbnail }}
|
||||||
{{ $img := $base.Filter (images.Overlay (.Process "fit 300x250") 875 38 )}}
|
{{ $thumbnail := $.Resources.Get . }}
|
||||||
{{ $img = $img.Filter (images.Text $.Page.Title (dict
|
{{ with $thumbnail }}
|
||||||
|
{{ $img = $img.Filter (images.Overlay (.Process "fit 300x250") 875 38 )}}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ $img = $img.Filter (images.Text $text (dict
|
||||||
"color" "#d8d8d8"
|
"color" "#d8d8d8"
|
||||||
"size" 64
|
"size" 64
|
||||||
"linespacing" 2
|
"linespacing" 2
|
||||||
|
@ -37,22 +34,6 @@
|
||||||
"font" $font
|
"font" $font
|
||||||
))}}
|
))}}
|
||||||
{{ $img = resources.Copy (path.Join $.Page.RelPermalink "og.png") $img }}
|
{{ $img = resources.Copy (path.Join $.Page.RelPermalink "og.png") $img }}
|
||||||
{{ $.Scratch.Set "og_image" $img }}
|
|
||||||
{{ end }}
|
|
||||||
{{ else }}
|
|
||||||
{{ $img := $base.Filter (images.Text .Page.Title (dict
|
|
||||||
"color" "#d8d8d8"
|
|
||||||
"size" 64
|
|
||||||
"linespacing" 2
|
|
||||||
"x" 40
|
|
||||||
"y" 300
|
|
||||||
"font" $font
|
|
||||||
))}}
|
|
||||||
{{ $img = resources.Copy (path.Join $.Page.RelPermalink "og.png") $img }}
|
|
||||||
{{ .Scratch.Set "og_image" $img }}
|
|
||||||
{{ end }}
|
|
||||||
{{- end -}}
|
|
||||||
{{ $img := .Scratch.Get "og_image" }}
|
|
||||||
|
|
||||||
<meta property="og:image" content="{{$img.Permalink}}">
|
<meta property="og:image" content="{{$img.Permalink}}">
|
||||||
<meta property="og:image:width" content="{{$img.Width}}" />
|
<meta property="og:image:width" content="{{$img.Width}}" />
|
||||||
|
@ -67,4 +48,3 @@
|
||||||
{{- with .Params.videos }}{{- range . }}
|
{{- with .Params.videos }}{{- range . }}
|
||||||
<meta property="og:video" content="{{ . | absURL }}" />
|
<meta property="og:video" content="{{ . | absURL }}" />
|
||||||
{{ end }}{{ end }}
|
{{ end }}{{ end }}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue