Plugin for Twitter and Open Graph Cards

plugin-cards

A Micro.blog plugin for adding Twitter and Open Graph <meta> tags, which are used to generate link preview cards all over the g0dd@mn place. Its code lives here

There is only one template involved. It lives at /layouts/partials/twitter-open-graph-cards.html and looks like this:

{{/* Determine site name */}}
{{ $site_name := site.Title }}

{{/* Determine the title */}}
{{ $title := .Title }}

{{/* Determine description */}}
{{ $description := .Summary | default (.Description | default .Site.Params.description )  }}

{{/* Create a variable for the card image */}}
{{ $image := false }}

{{/* Check whether the page as an image */}}
{{ with .Params.images }}{{ $image = (index . 0 | absURL) }}{{ end }}

{{ if not $image }}

  {{/* Capture the path for the page */}}
  {{ $page_path := (urls.Parse .Permalink).Path }}
  
  {{/* Check whether card data is available */}}
  {{ with site.Params.card_data_json }}
    {{ with transform.Unmarshal . }}
    
      {{/* Check whether the data has an entry for this page */}}
      {{ with (index . $page_path) }}{{ $image = . }}{{ end }}
      
      {{/* Otherwise check for a default image */}}
      {{ if not $image }}
        {{ with index . "default" }}{{ $image = . }}{{ end }}
      {{ end }}
    
    {{ end }}
    
  {{ end }}
  
{{ end }}

{{/* Grab section and categories */}}
{{ $section := false }}
{{ $categories := false }}
{{ if .IsPage }}
  {{ if ne (len .Section) 0 }}{{ $section = .Section }}{{ end }}
  {{ if .Page.Params.categories }}{{ $categories = .Page.Params.categories }}{{ end }}
{{ end }}

{{/* Grab times */}}
{{ $iso8601 := "2006-01-02T15:04:05-07:00" }}
{{ $published_time := false }}
{{ $modified_time := false }}
{{ $updated_time := false }}
{{ if .IsPage }}
  {{ if not .PublishDate.IsZero }}
    {{ $published_time = (.PublishDate.Format $iso8601 | safeHTML) }}
  {{ else if not .Date.IsZero }}
    {{ $published_time = (.Date.Format $iso8601 | safeHTML) }}
  {{ end }}
  {{ if not .Lastmod.IsZero }}{{ $modified_time = (.Lastmod.Format $iso8601 | safeHTML) }}{{ end }}
{{ else if not .Date.IsZero }}
  {{ $updated_time = (.Date.Format $iso8601 | safeHTML) }}
{{ end }}

{{/* Determine type */}}
{{ $type := "website" }}
{{ $audio := false }}
{{ with .Params.audio }}
{{ $audio = (index . 0 | absURL) }}
{{ $type = "music.song" }}
{{ else }}
{{ if .Title }}{{ $type = "article" }}{{ end }}
{{ end }}

{{/* Create meta tags using the variable values. */}}
<meta property="og:url" content="{{ .Permalink }}" />
<meta property="og:site_name" content="{{ $site_name }}" />
{{ if $title }}
<meta property="og:title" content="{{ $title }}" />
{{ else }}
<meta property="og:title" content="{{ $description }}" />
{{ end }}
<meta property="og:description" content="{{ $description }}" />
{{ with $image }}<meta property="og:image" content="{{ . }}" />{{ end }}
{{ with $published_time }}<meta property="article:published_time" content="{{ . }}" />{{ end }}
{{ with $modified_time }}<meta property="article:modified_time" content="{{ . }}" />{{ end }}
{{ with $section }}<meta property="article:section" content="{{ . }}" />{{ end }}
{{ with $categories }}{{ range first 6 . }}<meta property="article:tag" content="{{ . }}" />{{ end }}{{ end }}
{{ with $updated_time }}<meta property="og:updated_time" content="{{ . }}" />{{ end }}
{{ with $audio }}<meta property="og:audio" content="{{ . }}" />{{ end }}
<meta property="og:type" content="{{ $type }}" />
{{ if $image }}
<meta name="twitter:card" content="summary_large_image"/>
{{ else }}
<meta name="twitter:card" content="summary"/>
{{ end }}
{{ with site.Params.twitter_username }}
<meta name="twitter:site" content="@{{ . }}" />
<meta name="twitter:creator" content="@{{ . }}" />
{{ end }}
<meta property="twitter:domain" content="{{ (urls.Parse site.BaseURL).Host }}">
<meta property="twitter:url" content="{{ .Permalink }}">
{{ if $title }}
<meta name="twitter:title" content="{{ $title }}" />
{{ else }}
<meta name="twitter:title" content="{{ $description }}" />
{{ end }}
<meta name="twitter:description" content="{{ $description }}" />
{{ with $image }}<meta property="twitter:image" content="{{ . }}" />{{ end }}

Open Graph audio meta tags are created when an audio file is detected.

Plugin Parameters

The Twitter Username parameter establishes the content creator for Twitter cards. If you leave this empty, the plugin will fall back to site.Params.twitter_username, if that has been set. Without one of these two variables holding a value, Twitter cards will not be generated.

The Card Data parameter is optional. To understand why it is there, let’s talk about card images.

Card Image

The first image found gets priority. The large summary Twitter card will be generated when there is an image available; otherwise, the smaller summary card is generated.

When the page for which you are generating cards does not have an image available in its front matter, that’s when cool sh$t happens … and why that Card Data parameter is there.

The data consists of a JSON object stored as a string (which is what happens when you paste your JSON object code into the field and hit the button). The pasted code might look something like this:

{
  "default": "https://moondeer.blog/uploads/2021/7c412827ad.jpg",
  "/plausible/": "https://moondeer.blog/uploads/2021/e71e7d47c1.jpg",
  "/2021/": "https://moondeer.blog/uploads/2021/98295e13a8.jpg",
  "/2020/": "https://moondeer.blog/uploads/2021/24760a1062.jpg",
  "/about/": "https://moondeer.blog/uploads/2021/955619b235.jpg",
  "/cloud/": "https://moondeer.blog/uploads/2021/547d825d8a.jpg",
  "/bookshelf/": "https://moondeer.blog/uploads/2021/27a279361f.jpg",
  "/gallery/":"https://moondeer.blog/uploads/2021/8585a4a081.jpg",
  "/categories/perspectives/": "https://moondeer.blog/uploads/2021/f5f64b49bb.jpg",
  "/categories/projects/": "https://moondeer.blog/uploads/2021/a0c8728c89.jpg",
  "/categories/poetry/": "https://moondeer.blog/uploads/2021/23a2035cdc.jpg",
  "/categories/music/": "https://moondeer.blog/uploads/2021/8d0a055caa.jpg",
  "/categories/programming/": "https://moondeer.blog/uploads/2021/47e02e5e74.jpg",
  "/categories/critters/": "https://moondeer.blog/uploads/2021/0a37500db6.jpg",
  "/categories/stream-of-consciousness/": "https://moondeer.blog/uploads/2021/c11b3de2ff.jpg",
  "/categories/inside-the-art/": "https://moondeer.blog/uploads/2021/8c4669346c.jpg",
  "/categories/biographical-tripe/": "https://moondeer.blog/uploads/2021/92a565154b.jpg",
  "/categories/artsy-fartsy/": "https://moondeer.blog/uploads/2021/76f1f5d0d6.jpg",
  "/categories/personal-favorites/": "https://moondeer.blog/uploads/2021/328c442bd6.jpg"
}

All the values are image URLs. As for the keys, there are really only two things happening here. There is a default entry, which is used whenever nothing else could be retrieved. All the remaining keys are site paths. The bits that follow [SCHEME://HOSTNAME]. This is how your pages without images in the front matter get their image card.

Starting with version 2, instead of pasting all that JSON into the parameter field, you can save it into a template file located at data/plugin_cards/card_data.json.

Soooo … when I leave a link like this: https://moondeer.blog/gallery/

I get a Twitter card like this:

Orrrr … say I leave a link like this: https://moondeer.blog/categories/perspectives/

Then I get a Twitter card like this:

1 Like

This looks fantastic! How do I get this working?

2 Likes

I’ll come back and update this reply with a walkthrough in the morning.

Minor update: the short answer is to go to the design tab, click ‘edit custom themes’, click ‘new plugin’, give it a name, enter ‘https://github.com/moonbuck/plugin-cards’ in the repository field, click the button to create, follow along with the README, and than report back with any questions.

So am I correct in saying the primary difference over the existing open graph plug-in is the ability to use a JSON config to set default images for certain routes when there is no image on the page?

correct

I tried it with a default image using the plugin configuration UI. It worked well in a tweet, but I got this error in my design tab.

Let’s both do fresh installs (not sure the last time I touched it up) and I’ll help track this down. I’ve had Hugo randomly try parsing YAML that isn’t there before.

Update: my fresh install is working. Let’s try it using a data file (my new favorite method, the plugin interface stores the data as a string). We want to create a new template file by going to the design tab, going to ‘edit custom themes’, clicking on the plugin, and clicking on ‘new template’. The location will be data/plugin_cards/cards_data.json. Enter the JSON object like you did in the interface. My example would be:

{
  "/2020/": "https://moondeer.blog/uploads/2021/24760a1062.jpg",
  "/2021/": "https://moondeer.blog/uploads/2021/98295e13a8.jpg",
  "/about/": "https://moondeer.blog/uploads/2021/955619b235.jpg",
  "/artsy-fartsy/": "https://moondeer.blog/uploads/2021/76f1f5d0d6.jpg",
  "/biographical-tripe/": "https://moondeer.blog/uploads/2021/92a565154b.jpg",
  "/bookshelf/": "https://moondeer.blog/uploads/2021/27a279361f.jpg",
  "/categories/artsy-fartsy/": "https://moondeer.blog/uploads/2021/76f1f5d0d6.jpg",
  "/categories/biographical-tripe/": "https://moondeer.blog/uploads/2021/92a565154b.jpg",
  "/categories/critters/": "https://moondeer.blog/uploads/2021/0a37500db6.jpg",
  "/categories/inside-the-art/": "https://moondeer.blog/uploads/2021/8c4669346c.jpg",
  "/categories/music/": "https://moondeer.blog/uploads/2021/8d0a055caa.jpg",
  "/categories/personal-favorites/": "https://moondeer.blog/uploads/2021/328c442bd6.jpg",
  "/categories/perspectives/": "https://moondeer.blog/uploads/2021/f5f64b49bb.jpg",
  "/categories/poetry/": "https://moondeer.blog/uploads/2021/23a2035cdc.jpg",
  "/categories/programming/": "https://moondeer.blog/uploads/2021/47e02e5e74.jpg",
  "/categories/projects/": "https://moondeer.blog/uploads/2021/a0c8728c89.jpg",
  "/categories/stream-of-consciousness/": "https://moondeer.blog/uploads/2021/c11b3de2ff.jpg",
  "/cloud/": "https://moondeer.blog/uploads/2021/547d825d8a.jpg",
  "/critters/": "https://moondeer.blog/uploads/2021/0a37500db6.jpg",
  "/gallery/": "https://moondeer.blog/uploads/2021/8585a4a081.jpg",
  "/inside-the-art/": "https://moondeer.blog/uploads/2021/8c4669346c.jpg",
  "/music/": "https://moondeer.blog/uploads/2021/8d0a055caa.jpg",
  "/personal-favorites/": "https://moondeer.blog/uploads/2021/328c442bd6.jpg",
  "/perspectives/": "https://moondeer.blog/uploads/2021/f5f64b49bb.jpg",
  "/plausible/": "https://moondeer.blog/uploads/2021/e71e7d47c1.jpg",
  "/poetry/": "https://moondeer.blog/uploads/2021/23a2035cdc.jpg",
  "/programming/": "https://moondeer.blog/uploads/2021/47e02e5e74.jpg",
  "/projects/": "https://moondeer.blog/uploads/2021/a0c8728c89.jpg",
  "/stream-of-consciousness/": "https://moondeer.blog/uploads/2021/c11b3de2ff.jpg",
  "default": "https://moondeer.blog/uploads/2021/7c412827ad.jpg"
}

With the file in place, the code throwing the error will be bypassed as we won’t need to convert from a string to a map:

  {{- if (and $card_data (not ($card_data | reflect.IsMap))) }}
  {{- with transform.Unmarshal $card_data }}
  {{- $card_data = . }}
  {{- end }}
  {{- end }}

UPDATE Since I have come to prefer using data files rather than the plugin parameter interface, I wrote a guide this morning with this plugin as my example: Feeding Data to My Plugins

A couple of days ago I decided to mess around with structured data for Google searches and such. I created it as a separate repository but I probably oughta just roll it into the cards plugin since they are so aligned. It is currently hanging out at plugin-structured-data

I tried with a fresh install, but I couldn’t get things to work by plugging the default and categories into the JSON file. All I was getting seemed to be cached from my previous install.

Okay, I’ll help you troubleshoot. Twitter can be hella finicky (my twitter cards seem to be dependent on my Cloudflare setup to locate my images, no idea why, didn’t used to be that way). Let’s get some more context:

  1. Check whether there is any kind of card generated for a link in something like Apple Messages.

  2. Check whether a card is generated for a post with an image versus a link where we want to lookup the image using our data.

  3. See if Twitter’s card validator has anything to say. (More wonkiness here, half the time the first click on the validate button fetches my card without an image. Click it again and suddenly it finds the image.

I also want to move the data from the plugin parameter interface to a data file. This will eliminate any chance of that build error from having to parse the data from a string.

Go to the design tab and scroll down to the edit custom themes button. You should be able to find the plugin in the list of installed themes. Select it and then hit new template. Set the location as data/plugin_cards/data.toml. (This could also be stored as YAML or JSON, I have come to prefer TOML since I needn’t worry about brackets or indentation.) Entering the data is straightforward. Here is the current contents of my file (I store mine in my custom theme, if you have a custom theme you can create the file at data/plugin_cards_data.toml.)

TwitterUsername = "moondeerdotblog"
default = "https://moondeer.blog/uploads/2021/7c412827ad.jpg"
"/plausible/" = "https://moondeer.blog/uploads/2021/e71e7d47c1.jpg"
"/2021/" = "https://moondeer.blog/uploads/2021/98295e13a8.jpg"
"/2020/" = "https://moondeer.blog/uploads/2021/24760a1062.jpg"
"/about/" = "https://moondeer.blog/uploads/2021/955619b235.jpg"
"/cloud/" = "https://moondeer.blog/uploads/2021/547d825d8a.jpg"
"/bookshelf/" = "https://moondeer.blog/uploads/2021/27a279361f.jpg"
"/gallery/" = "https://moondeer.blog/uploads/2021/8585a4a081.jpg"
"/categories/perspectives/" = "https://moondeer.blog/uploads/2021/f5f64b49bb.jpg"
"/categories/projects/" = "https://moondeer.blog/uploads/2021/a0c8728c89.jpg"
"/categories/poetry/" = "https://moondeer.blog/uploads/2021/23a2035cdc.jpg"
"/categories/music/" = "https://moondeer.blog/uploads/2021/8d0a055caa.jpg"
"/categories/programming/" = "https://moondeer.blog/uploads/2021/47e02e5e74.jpg"
"/categories/critters/" = "https://moondeer.blog/uploads/2021/0a37500db6.jpg"
"/categories/stream-of-consciousness/" = "https://moondeer.blog/uploads/2021/c11b3de2ff.jpg"
"/categories/inside-the-art/" = "https://moondeer.blog/uploads/2021/8c4669346c.jpg"
"/categories/biographical-tripe/" = "https://moondeer.blog/uploads/2021/92a565154b.jpg"
"/categories/artsy-fartsy/" = "https://moondeer.blog/uploads/2021/76f1f5d0d6.jpg"
"/categories/personal-favorites/" = "https://moondeer.blog/uploads/2021/328c442bd6.jpg"
"/perspectives/" = "https://moondeer.blog/uploads/2021/f5f64b49bb.jpg"
"/projects/" = "https://moondeer.blog/uploads/2021/a0c8728c89.jpg"
"/poetry/" = "https://moondeer.blog/uploads/2021/23a2035cdc.jpg"
"/music/" = "https://moondeer.blog/uploads/2021/8d0a055caa.jpg"
"/programming/" = "https://moondeer.blog/uploads/2021/47e02e5e74.jpg"
"/critters/" = "https://moondeer.blog/uploads/2021/0a37500db6.jpg"
"/stream-of-consciousness/" = "https://moondeer.blog/uploads/2021/c11b3de2ff.jpg"
"/inside-the-art/" = "https://moondeer.blog/uploads/2021/8c4669346c.jpg"
"/biographical-tripe/" = "https://moondeer.blog/uploads/2021/92a565154b.jpg"
"/artsy-fartsy/" = "https://moondeer.blog/uploads/2021/76f1f5d0d6.jpg"
"/personal-favorites/" = "https://moondeer.blog/uploads/2021/328c442bd6.jpg"

We’ll see where to go from here once you’ve experimented with some of the above.

Got it working, thanks! Twitter has been flaky with images from Micro.blog and Blot.im. I never seem to have any problems with open graph posted in an app like Messages or Teams. It’s just Twitter. It never happens with Ghost, though. I can tweet the same post and it always picks up the image from Ghost.

1 Like

I was beating my head against a wall trying to figure out why Twitter was the only platform not fetching my images. I figured it had to be an issue with their “card crawler” but I had no idea what to do about it. I had just started using Cloudflare to get a site hosted for me by fineartamerica.com under my domain name and figured I may as well try using Cloudflare with my Micro.blog site. As soon as I did, Twitter magically began finding my images again. My best guess is that it has to do with SSL or certificates or something.

1 Like