Walking Through plugin-cards
Y’all asked for it … now you’re getting it. Here comes a walkthrough (aimed mostly at folks that haven’t spent the last six months as a part-time reverse engineer figuring out how Micro.blog-flavored Hugo works under the hood) of my Micro.blog plugin for generating preview cards, creatively named plugin-cards.
Okay, I’m in … wait … what do I need to do … wait … what does the plugin do?
What the Plugin Does
The functionality provided by the latest iteration of this plugin is kinda modular. The architecture can be thought of as a collection of three separate modules, each aimed at a particular target:
- TwitterOG
- Help social media platforms make snazzy little preview cards when we drop in links to our site
- StructuredData
- Help search engines create richer results for our blog pages
- Cardify
- Provide our own snazzy little preview cards when we drop our links into our own posts and post-lists
Now I’m really excited to show you all the awesomesauce under the hood … the configuration file, the…
*blink* *blink*
actually … why don’t we start by looking at what oughta happen right out the box without you having to do a thing (and do let me know should your mileage vary, as I only possess enough patience for testing sh$te with my own rig). Let’s start with what is currently the least configurable of the plugin’s modules.
Structured Data
Wait … WTF is structured data?
Structured data is the sh$te we inject into the page <head>
for search engines to sniff out. If a search engine smells something it likes, it may provide a richer result for the page when it comes up in a search. Whether or not it does is out of our hands; but, we can check whether it has access to the data it would need (more on this in a second). A good place to look for figuring out WTF is Google’s gallery of examples.
Much may be added to this module in the future, depending on the level of interest in the various examples in that gallery. Currently, the only feature implemented is the Article feature. Since this feature only makes sense for blog posts, only blog post pages currently have structured data generated and injected in the page <head>
.
The plugin doesn’t do much in the way of manufacturing missing attributes like a post title or summary (this differs from the plugin’s behavior regarding preview cards) … so let’s look at an example using one of my prototypical titled posts: On What is Missing.
The Data … You Know … Structured
Given the aforementioned post, the plugin grabs what it can in the way of attributes and maps it to the Article schema as interpreted by Google.
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"author": {
"@type": "Person",
"name": "Jason Cardwell",
"url": "https://moondeer.blog/about/"
},
"dateModified": "2022-01-31T14:24:59+00:00",
"datePublished": "2021-10-17T12:25:00-08:00",
"description": "An idea under construction",
"headline": "On What is Missing",
"image": [
"https://moondeer.blog/uploads/2021/782087f3c2.jpg"
],
"wordcount": 602
}
Any images in the post oughta get mapped to the image array. The post title maps to headline. The published date gets picked up along with the last modification (you may need to add something like lastmod = ['lastmod', ':fileModTime', ':default']
to your configuration's frontmatter
setting to get anything useful as a modifcation date). The author name oughta get picked up automatically. To get the url attribute included for author you’ll need to set a parameter value in the plugin’s config file; but, I’ll hold off explaining how you configure the plugin until later.
The Payoff?
Okay … so … did I grab a screenshot of a Google search showing this post listed more richly than usual?
No. No … I did not.
But I did grab screenshots proving Google had the f$&kin’ option
Google provides a rich results test page where you can drop in your link, kinda like:
Wait for it to do its thing and then it will be all:
Expanding detected items lets us see whether it sniffed out what we left for it to find.
Right. Now you’re done with structured data. Totally a set-it-and-forget-it kinda deal. Moving on.
So … when you say I need to fill in a parameter value…
Nope. Not there yet. Keep it in your pocket. Moving on.
Twitter and Open Graph <meta>
Tags
Now for the stuff that probably brought you here in the first place. It, too, used to be a set-it-and-forget-it kinda deal … but now you gots options and additional capabilities you’ll want to know how to take advantage of (prepositional-sentence-ending outlaw
A brief note about screenshots before we begin To simplify screenshotting all this sh$te I’ve narrowed my sources down to three places, the stupid Facebook debugger, the Twitter Card Validator, and a conversation I’m having with myself in Apple Messages. Every platform showing preview cards is basically crawling for <meta>
tags in one of these two formats.
Right. With that out of the way let’s look at what remains of Open Graph and Twitter Card set-it-and-forget-it bits.
Wait … WTF is an Open Graph*?*
Open Graph is the protocol Facebook came up with for this preview card sh$te.
Okay … and WTF is a Twitter Card*?*
Twitter Cards are how Jack handled this whole preview card sh$te.
We cool? Good. Moving along.
The plugin automatically generates Open Graph and Twitter Card <meta>
tags for every page on your site, injecting them into the page <head>
. The coolness-to-configuration ratio varies depending on the page. Let’s begin with the prototypical post for which you needn’t configure sh$t.
The Prototypical Previewable Post
Once again we’ll turn to On What is Missing for that set-it-and-forget-it feel-good example.
Everything the plugin really needs to generate a cool summary card is found within the page’s front matter (which is something you needn’t ever actually understand … learn this sh$t at your own discretion) and in the first three lines of post content.
![](https://moondeer.blog/uploads/2021/782087f3c2.jpg)
*An idea under construction*
The plugin takes things like publish date, site name, section, categories, and the page url from the page’s front matter.
It takes the first image it finds (currently also via the page front matter … but the image is only in the front matter ‘cause you stuck it in your post and @mantion injected it into the front matter for you).
The post’s summary becomes the description (truncated to 200 characters or less).
The post’s title will act as, you know, the title (truncated to 70 characters or less). For posts without a title the plugin will use up to the first 70 characters of whatever it landed on for the description.
Let me see the tags.
The Tags
For the post above the plugin will generate the following Open Graph tags…
<meta property="article:published_time" content="2021-10-17T12:25:00-08:00">
<meta property="article:modified_time" content="2022-01-31T14:24:59+00:00">
<meta property="article:section" content="2021">
<meta property="article:tag" content="Perspectives">
<meta property="article:tag" content="Pinned">
<meta property="og:type" content="article">
<meta property="og:url" content="https://moondeer.blog/2021/10/17/on-what-is.html">
<meta property="og:title" content="On What is Missing">
<meta property="og:description" content="An idea under construction">
<meta property="og:site_name" content="On the Mind of Moondeer">
<meta property="og:image" content="https://moondeer.blog/uploads/2021/782087f3c2.jpg">
along with these Twitter Card tags:
<meta name="twitter:url" content="https://moondeer.blog/2021/10/17/on-what-is.html">
<meta name="twitter:title" content="On What is Missing">
<meta name="twitter:description" content="An idea under construction">
<meta name="twitter:site" content="@moondeerdotblog">
<meta name="twitter:creator" content="@moondeerdotblog">
<meta name="twitter:domain" content="moondeer.blog">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://moondeer.blog/uploads/2021/782087f3c2.jpg">
It also happens to generate a custom tag (the reason for which shall just have to remain a mystery until we breach the topic of module three):
<meta property="article:reading_time" content="3">
The Payoff
With the tags above in place, Facebook will spit out a card kinda like… Twitter gives you a card kinda like… and Apple Messages follows suit with a card kinda like:
Downsizing
Twitter offers two version of its summary card: large and small. By default, the plugin will go big when it has an image to work with and go small for list pages and single pages without an image. You can register your preference in the plugin’s config file to control this default behavior.
Were I to tell the plugin I preferred the smaller summary cards, Twitter would give me a card kinda like:
Again with this config file business, what exactl…
Noooopppe. Not there yet. Keep it in your pants. How’s about we look at a some minimal effort variations, audio and video preview cards via the included Hugo shortcodes?
Audio and Video, Oh My
For audio and video preview cards, I settled on an opt-in strategy. Were the process automatic, I wouldn’t be able to include audio or video assets in the body of my essays without triggering the generation of an inappropriate preview card. Opting in is easy anyway. Let’s take a peak at a pair of examples.
How It Works
The easily overwhelmed may skip over this section without any detriment to their ability to utilize the plugin. Learn at your own discretion1.
To get the plugin to generate a particular type of preview card for a specific post, you need only tell it you wish it to do so.
Okay … whatever smarta$$ … and how TF do I do that exactly?
Glad you asked. This is how.
Query Parameters
Great … what the ever-loving-f$&k are query parameters?
Query parameters are the building blocks composing a query string.
F$&k you.
I know. I’m an a$$hole. Okay, so a URL’s query string is basically the balderdash at its tail that begins with ?
followed by &
delimited parameters in the form of name
or name=value
.
The cool thing about query strings is you can basically add them to any URL you want and browsers will simply ignore anything they aren’t actively looking for (prepositional-sentence-ending outlaw
The other cool thing about query strings is that I can code the plugin to look for them.
Say you are dropping an audio file in your post kinda like.
<audio src="https://Moondeer.micro.blog/uploads/2020/b7dafae583.mp3"
controls
preload="metadata"></audio>
Say the audio happens to be the main attraction for this particular post; and, you’re all, “kinda be cool if the post’s preview card played the audio.”
The magic happens when you tack on ?card-audio
to the end of your audio source:
<audio src="https://Moondeer.micro.blog/uploads/2020/b7dafae583.mp3?card-audio"
controls
preload="metadata"></audio>
You’ve just told the plugin what you want my friend. The exact same strategy is used for triggering video preview cards:
<video src="https://moondeer.blog/uploads/2021/497463855c.mov?card-video"
playsinline
autoplay
muted
loop></video>
Data Attributes
The use of data attributes is another way I could have gone. I thought of query parameters first. I only thought of data attributes when I wanted to inform the plugin that I had a title I wanted it to use when generating the <meta>
tags for a post without a title. This same need is met a different way that I’ll get to later; but, I may as well show you the first solution.
Say you have a post without a title … for which you do not wish to set a title as doing so will change the way it cross-posts to the Micro.blog timeline. Say you don’t event want the title to show up in the post.
Not seein’ it bud.
Okay, whatever … for some f$&kin’ reason this is what I wanted at one point so I made the plugin hip to the use of something like:
<h1 data-card-title="Wind Rider" style="display: none"></h1>
Like I said, I obsoleted the corresponding shortcode almost instantly … but … the plugin will still parse out data-card-title
attributes (regardless of whether the element is visible) should anyone happen to have a header lying around in a post they wish to serve as the card title. For specifying the title invisibly, I found a better way (more on this when we get to the video example).
Pics … pics or it didn’t happen
Right, then … to the examples
An Old Original Song
When I first created the blog I came across an old song I made whilst nostalgically looking through all my sh$t. I wanted to give it some inline-playable-cardification2. Way back then (like a year ago) … my approach was awkward. Now … it’s less awkward.
As I laid out in the Query Parameters section, there really isn’t anything to this but adding ?card-audio
to the end of the audio source’s URL.
To simplify the process even further, I created a shortcode that handles the creation of the <audio>
element.
The Post
Quick side note, you'll find an example with the updated version of this post in the Appendix (assuming I get around to documenting it for ya).
Using the shortcode, the post amounts to this…
_An old original song from 2006 that I recently rediscovered_
The HTML
The rendered HTML comes out like this:
<p><em>An old original song from 2006 that I recently rediscovered</em></p>
<audio src="https://Moondeer.micro.blog/uploads/2020/b7dafae583.mp3?card-audio" controls="controls" preload="metadata"></audio>
The Tags
With the above content, the plugin will generate the following Open Graph tags…
<meta property="og:url" content="https://moondeer.blog/2020/11/26/an-old-original.html">
<meta property="og:title" content="An old original song from 2006 that I recently rediscovered">
<meta property="og:description" content="An old original song from 2006 that I recently rediscovered">
<meta property="og:site_name" content="On the Mind of Moondeer">
<meta property="article:published_time" content="2021-12-12T15:17:00-08:00">
<meta property="article:section" content="2020">
<meta property="article:tag" content="Music">
<meta property="article:tag" content="Pinned">
<meta property="og:audio" content="https://Moondeer.micro.blog/uploads/2020/b7dafae583.mp3">
<meta property="og:type" content="music.song">
as well as the following Twitter Card tags:
<meta name="twitter:url" content="https://moondeer.blog/2020/11/26/an-old-original.html">
<meta name="twitter:title" content="An old original song from 2006 that I recently rediscovered">
<meta name="twitter:description" content="An old original song from 2006 that I recently rediscovered">
<meta name="twitter:site" content="@moondeerdotblog">
<meta name="twitter:creator" content="@moondeerdotblog">
<meta name="twitter:domain" content="moondeer.blog">
<meta name="twitter:card" content="player">
<meta name="twitter:player" content="https://Moondeer.micro.blog/uploads/2020/b7dafae583.mp3">
<meta name="twitter:player:width" content="338">
<meta name="twitter:player:height" content="338">
Side note: I’m still kinda figuring out the right width and height for f$&kin’ audio … way to make it weird, Jack.
The Payoff
What this all amounts to is a Facebook card that sucks the hardest… a Twitter card that starts out cool… before falling into the suck… and an Apple Messages card… that acts f$&king appropriately:
Okay … okay … so what about video?
Glad you asked … ‘cause Imma free two birds with one stone (f$&k killing them, Imma rewrite colloquialism … the birds were locked in a cage, you see? I freed those little f$&kers by rendering the cage ineffective with my stone):
🄐 Imma show you that example with video…
and
⑵ Imma get unnecessarily jiggy wit’ it to show you what I meant about obsoleting that data-attribute sh$te from early. Buckle up buttercup.
Murphy Catches a Breeze
I’ll have to admit. This one is kinda crafty. I wanted to be able to have Twitter player cards for my critter videos. I want the cards to have titles … but not the posts (which borks the Micro.blog timeline content). I made that sh$t happen and created some shortcodes to make it easily repeatable.
The Post
We’ll use a card-video
shortcode much like the card-audio
shortcode from earlier and a new card-config
shortcode capable of specifying values for any of the following attributes: title, description, image, audio, video, and type. We’ll use the card-config
shortcode to invisibly set the title and image (just a cropped screenshot of one of the frames of video). We’ll let the post’s visible text serve as the description; and, we’ll use the card-video
shortcode to create the <video>
element. What this amounts to is a post that looks like this:
Murphy catches a breeze.
First off, notice the autolink with which we must do battle. You save the post all like image="https://moondeer.blog/uploads/2021/abe98ed250.jpg"
; but, if you reopen it you’ll find it was processed and saved all like image="[moondeer.blog/uploads/2...](https://moondeer.blog/uploads/2021/abe98ed250.jpg)"
. Whatevs …we parse what we need out of the markdown link and move on.
The HTML
Here’s what these two little shortcodes render to HTML:
<p>
<!--card-config
title=Wind Rider
image=[moondeer.blog/uploads/2...](https://moondeer.blog/uploads/2021/abe98ed250.jpg)
-->
Murphy catches a breeze.</p>
<video src="https://moondeer.blog/uploads/2021/497463855c.mov?card-video" playsinline="" autoplay="" muted="" loop=""></video>
Sooo … the <video>
tag looks an awful lot like that last <audio>
tag. Same trick: query parameter.
The cool sh$t is up top … do you see what I did there? That card-config
shortcode injects an HTML comment (sneaking it by the Hugo parser) telling the plugin what we want …invisibly.
The Tags
With the above content, the plugin will spit out these Open Graph tags…
<meta property="og:url" content="https://moondeer.blog/2021/12/09/murphy-catches-a.html">
<meta property="og:title" content="Wind Rider">
<meta property="og:description" content="Murphy catches a breeze.">
<meta property="og:site_name" content="On the Mind of Moondeer">
<meta property="article:published_time" content="2021-12-09T16:24:00-08:00">
<meta property="article:section" content="2021">
<meta property="article:tag" content="Critters">
<meta property="og:type" content="video.other">
<meta property="og:video" content="https://moondeer.blog/uploads/2021/497463855c.mov">
<meta property="og:video:width" content="600">
<meta property="og:video:height" content="338">
<meta property="og:image" content="https://moondeer.blog/uploads/2021/abe98ed250.jpg">
and these Twitter Card tags:
<meta name="twitter:url" content="https://moondeer.blog/2021/12/09/murphy-catches-a.html">
<meta name="twitter:title" content="Wind Rider">
<meta name="twitter:description" content="Murphy catches a breeze.">
<meta name="twitter:site" content="@moondeerdotblog">
<meta name="twitter:creator" content="@moondeerdotblog">
<meta name="twitter:domain" content="moondeer.blog">
<meta name="twitter:card" content="player">
<meta name="twitter:player" content="https://moondeer.blog/uploads/2021/497463855c.mov">
<meta name="twitter:player:width" content="600">
<meta name="twitter:player:height" content="338">
<meta name="twitter:image" content="https://moondeer.blog/uploads/2021/abe98ed250.jpg">
The Payoff
What this all amounts to is a Facebook card still sucking the hardest… a Twitter player card that finally makes sense… expanding into… and an Apple Messages card that remains consistent:
Okay … now I’m excited. Do you have something new and unfamiliar to show me that could knock me back a few feet?
I’ve got just the thing … it’s time to discuss the config files.
The Configuration Files
Sooooo, rather than continue to support a zillion locations for plugin parameter values, Imma go ahead and break y’all in with regard to the configuration file paradigm I’ve developed.
Of course you are … why make it easy on us?
To quote the ever-eloquent Shoresy, “give your balls a tug.” You’ll thank me when it’s over (though what you thank me for will vary).
The Tease
I believe the easiest way to understand Hugo data templates3 is through example. Of the three file formats Hugo supports for data templates (JSON4, TOML, and YAML5), I suppose JSON rings most familiar … sooo … Imma toss a big scary chunk of JSON at you. After which, I will show yous guys how this JSON object (which was injected into the page <head>
inside an HTML comment for one of my posts on account of a DebugPrint
parameter set to true
) was generated by fetching values out of the various data templates.
You’d think right about now you’d be seeing the contents of that JSON object. Nope. Dumping an entirely different markup language on top o’ your head instead. Everyone, say hello to TOML.
Sounds scary and unfamiliar.
I’ll introduce you. You’ll be fine.
The plugin’s data templates are TOML files. I chose the TOML format because it allows for the inclusion of instructional comments and actually survives the Micro.blog repository cloning process6. This means I can include templates that amount to documented form fields for you to fill in or ignore entirely to suit your fancy. Here comes a crash course in TOML syntax.
The TOML
The first thing you may notice in the TOML files I include are all the #
’s. These are used by TOML to denote comments. You can follow one with whatever TF you please up until the line breaks.
# plugin-cards config file
##########################
You needn’t really understand the format (in practice, all you’ll do is fill in the values where I tell you to via those comments); but, it may reduce the anxiety for some of y’all to know what you’re looking at. Let’s conjure some examples looking at equivalent TOML and JSON representations.
Top Level Entries
Say you wanted to store a few key-value pairs like whether you prefer pears to apples, the age you were at the onset of your first existential crisis, the days of the week your new trainer hands you your ass, and the name you gave your collected high school poetry.
In a JSON file we might store such information like this:
{
"PrefersPearsToApples": false,
"ExistentialCrisisOnsetAge": 42,
"DaysAssHanded": ["Tuesday", "Thursday", "Friday"],
"HighSchoolPoetryCollection": "Psalms for the Infidelic"
}
The same information stored in a TOML file might look like this:
PrefersPearsToApples = false
ExistentialCrisisOnsetAge = 42
DaysAssHanded = ["Tuesday", "Thursday", "Friday"]
HighSchoolPoetryCollection = "Psalms for the Infidelic"
The keys in a TOML file can be bare when composed entirely of ASCII letters, ASCII digits, underscores, and dashes ([A-Za-z0-9_-]
). Keys that include other characters are wrapped in single ('
) or double ("
) quotes.
The boolean values are true
and false
.
Number values are … well … numbers.
For our purposes, you can generally choose single or double quotes for string values. I’ll let the TOML documentation satiate the thirst for more on what options are available when storing string values.
And that’s top level entries in a nutshell. Let’s look at where TOML and JSON really start to diverge, nested entries.
Nested Entries
Say we want to build on our previously conjured example by adding a map of all the mascots of schools you’ve happened to attend. Then our JSON file might look something like:
{
"PrefersPearsToApples": false,
"ExistentialCrisisOnsetAge": 42,
"DaysAssHanded": ["Tuesday", "Thursday", "Friday"],
"HighSchoolPoetryCollection": "Psalms for the Infidelic",
"SchoolMascots": {
"ElementarySchool": "Indians",
"MiddleSchool": "Eagles",
"HighSchool": "Yellow Jackets",
"College": ["Yellow Jackets", "Owls", "Bulldogs"]
}
}
The same information stored in a TOML file might look like this:
PrefersPearsToApples = false
ExistentialCrisisOnsetAge = 42
DaysAssHanded = ["Tuesday", "Thursday", "Friday"]
HighSchoolPoetryCollection = "Psalms for the Infidelic"
[SchoolMascots]
ElementarySchool = "Indians"
MiddleSchool = "Eagles"
HighSchool = "Yellow Jackets"
College = ["Yellow Jackets", "Owls", "Bulldogs"]
You see what we did there? TOML likes to keep everything on the level so we use square brackets [key]
to denote the start of a table to which every key-value pair to follow will belong (until another [key]
is encountered).
Now let’s say you wanted to record how the school mascots have aged culturally since you attended. In JSON we might do something like:
{
"PrefersPearsToApples": false,
"ExistentialCrisisOnsetAge": 42,
"DaysAssHanded": ["Tuesday", "Thursday", "Friday"],
"HighSchoolPoetryCollection": "Psalms for the Infidelic",
"SchoolMascots": {
"ElementarySchool": "Indians",
"MiddleSchool": "Eagles",
"HighSchool": "Yellow Jackets",
"College": ["Yellow Jackets", "Owls", "Bulldogs"],
"Aged": {
"Well": {
"MiddleSchool": "Eagles",
"HighSchool": "Yellow Jackets",
"College": ["Yellow Jackets", "Owls", "Bulldogs"]
},
"Poorly": {
"ElementarySchool": "Indians"
}
}
}
}
The same information stored in a TOML file might look like this:
PrefersPearsToApples = false
ExistentialCrisisOnsetAge = 42
DaysAssHanded = ["Tuesday", "Thursday", "Friday"]
HighSchoolPoetryCollection = "Psalms for the Infidelic"
[SchoolMascots]
ElementarySchool = "Indians"
MiddleSchool = "Eagles"
HighSchool = "Yellow Jackets"
College = ["Yellow Jackets", "Owls", "Bulldogs"]
[SchoolMascots.Aged.Well]
MiddleSchool = "Eagles"
HighSchool = "Yellow Jackets"
College = ["Yellow Jackets", "Owls", "Bulldogs"]
[SchoolMascots.Aged.Poorly]
ElementarySchool = "Indians"
I suppose I could go on but that oughta be more than enough considering what I’ll be asking you to do. The TOML Spec does a better job explaining itself anywho. Let’s get back to that biga$$ JSON object representing like all of the plugin’s possible parameters.
The JSON Object Representing Like All of the Plugin’s Possible Parameters
Check it:
{
"Cardify": {
"Config": {
"Fingerprint": true,
"SassOutput": "nested"
},
"Style": {
"Body": "padding: 1rem;",
"Card": "background-color: white;\nborder-color: rgba(black, .125);\nborder-style: solid;",
"ClassName": "cardify-card",
"PublishDate": "color: #666;",
"ReadingTime": "color: #9091AB;",
"Text": "",
"Title": "margin-bottom: .5rem;",
"Variables": {
"BorderRadius": ".5rem",
"BorderWidth": "1px"
}
}
},
"Config": {
"DebugPrint": true,
"Version": "5.0.10"
},
"StructuredData": {
"Config": {
"AuthorName": "Jason Cardwell",
"Enable": true,
"ProfileURL": "https://moondeer.blog/about/"
}
},
"TwitterOG": {
"Audio": {
"Height": 338,
"Type": "music.song",
"Width": 338
},
"Config": {
"Enable": true
},
"Images": {
"/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/microblog": "https://moondeer.blog/uploads/2022/e4864eb5ed.jpg",
"/categories/music/": "https://moondeer.blog/uploads/2021/8d0a055caa.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",
"/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"
},
"Twitter": {
"Card": {
"Assignments": {
"/about/": "summary",
"/bookshelf/": "summary",
"/cloud/": "summary",
"/gallery/": "summary_large_image"
},
"Preference": {
"LargeSummaryRequiresImage": true,
"ListPage": "small",
"SinglePage": "large"
}
},
"Config": {
"Username": "moondeerdotblog"
},
},
"Video": {
"Height": 338,
"Type": "video.other",
"Width": 600
}
}
}
Here we have 18 pairs of curly braces (or was it 19, f$&k if I’m recounting), 252 quotation marks (give-or-f$&kin’-take), 52 or so commas (just don’t leave one trailing7), a few escaped newline characters, yada³. Were I asking you to maintain a configuration file that looked like this, you would need a code editor to avoid Hugo build errors stemming from the random misplaced or forgotten comma.
Thankfully, I needn’t ask you to do this on account of how Hugo allows us to modularize our data … automatically putting it back together as nested chunks of nonsense like this for the plugin to feed upon through the magic of file directories.
The Magic of File Directories
In place of some massive configuration file, you’ll find a directory (data/plugin_cards/
to be specific). Hugo views subdirectories of the data
directory as maps. Where the JSON object’s top level entries were for keys Config, TwitterOG, StructuredData, and Cardify, the top level of data/plugin_cards/
contains the file Config.toml
and three subdirectories: TwitterOG
, StructuredData
, and Cardify
.
Why Config.toml
instead of a Config
subdirectory? Because, within this branch of parameter values, there is nothing gained logistically from any further nesting. In fact, there really isn’t much to this file at all:
# plugin-cards config file
##########################
# The plugin version (printed to HTML comment when the plugin loads).
#
Version = '5.0.10'
# Whether to include HTML comments with debugging information
#
DebugPrint = false
Now, the mess of data stored in that JSON object under TwitterOG, on the other hand, could stand some modularization. Laying down the TwitterOG
subdirectory becomes synonymous with nesting the TwitterOG JSON object.
So what all do you need me to fill…
Stop. I really should have just gotten through the third module introduction before bringing this sh$t up; but, tell you what … I’ll show you just those settings alluded to earlier (when I let the f$&kin’ cat out of the bag).
The Bagless Cats
Configuring the Profile URL for Structured Data
Dip into the StructuredData
subdirectory and you will find another Config.toml
:
# Structured Data Parameters
############################
# Whether to inject an application/ld+json script in the page <head>
# with structured data for search engines.
#
Enable = true
# The value to set for author.name within the JSON object.
# Defaults to site.Author.name or site.Params.Author.name when empty
#
# AuthorName = ''
# The value to set for author.url within the JSON object.
# Defaults to site.Author.profileurl when empty
#
# ProfileURL = ''
Have you any idea of just which page you might wish to enter your service as profile ambassador?
Oh, you do, do you?
Well, be forewarned. If … and only if … you can manage to insert the URL for that page within the confines of the quotes introduced by ``# ProfileURL =``` and the remove that leading #
shall such service be bestowed.
Registering a Twitter Summary Card Preference
Drill down into the TwitterOG/Twitter/Card
subdirectory and you'll find a file named Preference.toml
:
# Twitter card type preferences
###############################
# The type of summary card to generate for single (non-list) pages.
# Has not affect on post pages configured for Twitter player cards.
# Valid options are small and large.
#
SinglePage = 'large'
# The type of summary card to generate for list pages.
# Valid options are small and large.
#
ListPage = 'small'
# Whether the generation of a large summary card should require
# an image (otherwise a small summary card will be generated instead).
#
LargeSummaryRequiresImage = true
Not much too it, am I right? The Hugo static site generator building our blogs lumps our pages into two categories: list pages and single pages. Here, then, we have a pair of key-value pairs for specifying whether we would prefer, when it falls upon the plugin to choose between a large or a small Twitter summary card, the large or the small summary card. We can register our preference for single pages … and we can register our preference for list pages.
The third key-value pair gives the plugin the ability to bail out of a large Twitter summary card were it to find itself unable to muster an image for it to display.
Simple right?
Whatever … you’re overcompensating with this much coddling.
Whatever … I was getting bored and wanted to spice this sh$t up. Strap on your big boy pants … it’s time to talk image data.
Image Data
Soooo … we’ve managed to configure some wicked cool preview cards for post pages … but what about all the other pages? You most certainly have a homepage. You likely have an about page. Unless you’re cool with preview cards for these pages being all…
then you’ll want to figure out a way to provide images for non-post pages…
perhaps even post pages that don’t contain any images…
Sold … what do you need me to do?
Glad you asked. I want you to find another file … adjacent to the config file we located earlier. Here is what finding the file looks like:
The file is located at data/plugin_cards/TwitterOG/Images.toml
and it starts out like this:
# Default image when the page has no image and there is not another match
#default = 'path-to-my-default-image'
# Path entries for non-post pages
# '/category/my-category/' = 'path-to-my-image'
If you want to have a fallback image to use when there aren’t any other images available, replace path-to-my-default-image
with the URL of the image you want to use and remove the #
from the start of the line.
For any other imageless page to which you’d like to assign an image, simply create a new key-value pair using the relative page path for the key and the image URL for the value. My file currently happens to look like this (this is a bald-faced lie … you'll understand once we get to Persisting your Plugin Configuration):
# Default image when the page has no image and there is not another match
#default = 'path-to-my-default-image'
default = "https://moondeer.blog/uploads/2021/7c412827ad.jpg"
# Path entries for non-post pages
# '/category/my-category/' = 'path-to-my-image'
"/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/microblog" = "https://moondeer.blog/uploads/2021/b8e46381e3.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"
Simple, right? Good … now who’s ready to find out about the third module and wrap this m0therf$&ker up?
Cardification
I do a lot of linking to previous posts. I thought they might look better all preview card style … so I made that sh$t happen. So, now, when I drop a link in a post like https://moondeer.blog/2021/09/07/on-the-american.html
(which the autolinking promptly converts to [https://moondeer.blog/2021/09/07/on-the-american.html](https://moondeer.blog/2021/09/07/on-the-american.html)
) … what I actually end up with when viewing the post on my blog is this:
I felt that some links may look better with a more compact, horizontal preview card … so I threw that in as well:
Yes … yes … daddy like (My bad, no idea why I would have you, the reader, saying something like that. I mean … you wouldn’t … would you … say something like that?)
Interesting … I mean … mildly. What feat fit for Heracles must I endeavor to complete for this to work?
Wow … sweet reference … daddy like. And this answer, my friend, is not-a-one. The generation of previews cards has been brought to you by … you guessed it … query parameters!
The Shortcode Approach
and you have yourself a preview card:
add horizontal
as the second parameter…
and you have yourself a horizontal preview card:
Okay … whatever … back to those query parameters.
Honestly, I’m barely paying attention at this point. Running out of steam, bud.
Samsies. Let’s look at the style hooks I gave y’all for adjusting the appearance of the preview cards and call it a f$&kin’ day.
Styling These M0therf$&kers
Sooo … I’ve left you these parameters to play with:
# Parameters for styling preview cards
######################################
# The class name to assign to the card wrapper.
#
ClassName = 'cardify-card'
# Sass block to apply to the card container.
#
Card = '''
background-color: white;
border-color: rgba(black, .125);
border-style: solid;
'''
# Sass block to apply to the card body.
#
Body = 'padding: 1rem;'
# Sass block to apply to the card title.
#
Title = 'margin-bottom: .5rem;'
# Sass block to apply to the card text.
#
Text = ''
# Sass block to apply to the reading time element.
#
ReadingTime = 'color: #9091AB;'
# Sass block to apply to the publish date element.
#
PublishDate = 'color: #666;'
[Variables]
# Width value for the border applied to card container.
#
BorderWidth = '1px'
# border-radius value for the card container.
#
BorderRadius = '.5rem'
These parameter values are then injected into the Sass file template used to generate the plugin’s stylesheet.
Appendix
Some bonus sh$te that may be of interest.
Twitter Summary Card Preference Revisited
I wanted to have some of my single-page Hugo pages use the small summary card while still defaulting to the large summary. So I added another data file for specifying Twitter Card types by page path. It’s located at data/plugin_cards/TwitterOG/Card/Assignments.toml
and it starts out like this:
# Twitter Cards assignments by path.
# The valid card options are player, summary, and summary_large_image
#
# For example, to generate a small summary card for an 'about' page
# when configured to generated large summary cards for all non-list pages,
# enter the path to the about page and the small summary card value: 'summary'
#
# '/about/' = 'summary'
Persisting Your Plugin Configuration
Kinda sucks having the hard work you’ve put into the configuration files thrown out every time the plugin updates. If you don’t have a custom theme yet, just jump into the fire already. When you create one, you’re basically duplicating whichever official theme you’ve been using. You are then free to create new files. So long as the paths you set for these new files do not collide with existing files (the entire list of which you’ll have at your avail) then you’re golden.
The reason I’m pushing you into a custom theme is so you can copy / paste the contents of the plugin’s data
directory into your theme. We need to alter the path a little so we avoid our own naming collisions. Here’s what we do. Take the contents of the plugin file located at data/plugin_cards/config.toml
and create yourself a new file in your custom theme located at data/plugin_cards_config.toml
. We basically just collapse a directory into an underscore. data/plugin_cards/images.toml
will become data/plugin_cards_images.toml
and data/plugin_cards/cards.toml
will become data/plugin_cards_cards.toml
.
The plugin will look for these custom theme files before falling back to the files provided by the plugin. Once created, you can delete and reinstall the plugin willy nilly without losing your configuration. It’s kinda how all my plugins work or I’d probably go apesh$t.
The Actual Contents of My File
When I lied about the current contents of my data/plugin_cards/TwitterOG/Images.toml
file, it was because my file looks exactly like your file. The real file I maintain lives inside my custom theme. It assembles all the config files back into a singular structure, not unlike that monster chunk of JSON I showed y'all earlier. It lives at data/plugin-cards.toml
and it looks like this:
######################################################################
# plugin-cards config file
######################################################################
# Top level parameters
#
# overrides data/plugin_cards/config.toml
[Config]
# Whether to include HTML comments with debugging information
#
# DebugPrint = false
# Twitter and Open Graph <meta> tag configuration
#
# overrides data/plugin_cards/twitterog/config.toml
#################################################
[TwitterOG.Config]
# Whether to inject Twitter and Open Graph <meta> tags into page <head>
#
# Enable = true
# Video parameters
#
# overrides data/plugin_cards/twitterog/video.toml
##################
[TwitterOG.Video]
# Default width for Open Graph and Twitter Card video tags.
#
# Width = 600
# Default height for Open Graph and Twitter Card video tags.
#
# Height = 338
# The default og:type value for Open Graph video tags.
#
# Type = 'video.other'
# Audio parameters
#
# overrides data/plugin_cards/twitterog/audio.toml
##################
[TwitterOG.Audio]
# Default width for Twitter player cards configured for audio.
#
# Width = 338
# Default height for Twitter player cards configured for audio.
#
# Height = 338
# The default og:type value for Open Graph audio tags.
#
# Type = 'music.song'
# Images to use for non-post pages and pages without an image.
#
# overrides data/plugin_cards/twitterog/images.toml
##############################################################
[TwitterOG.Images]
# Default image when the page has no image and there is not another match
#default = 'path-to-my-default-image'
default = "https://moondeer.blog/uploads/2021/7c412827ad.jpg"
# Path entries for non-post pages
# '/category/my-category/' = 'path-to-my-image'
"/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/microblog" = "https://moondeer.blog/uploads/2021/b8e46381e3.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"
# Twitter specific parameters
#
# overrides data/plugin_cards/twitterog/twitter/config.toml
#############################
[TwitterOG.Twitter]
# Username for Twitter <meta> tags.
# Defaults to site.Params.twitter_username when empty
#
# Config.Username = ''
# Twitter card type preferences
#
# overrides data/plugin_cards/twitterog/twitter/card/preference.toml
###############################
[TwitterOG.Twitter.Card.Preference]
# The type of summary card to generate for single (non-list) pages.
# Has not affect on post pages configured for Twitter player cards.
# Valid options are small and large.
#
# SinglePage = 'large'
# The type of summary card to generate for list pages.
# Valid options are small and large.
#
# ListPage = 'small'
# Whether the generation of a large summary card should require
# an image (otherwise a small summary card will be generated instead).
#
LargeSummaryRequiresImage = true
# Twitter Cards assignments by path.
# The valid card options are player, summary, and summary_large_image
#
# For example, to generate a small summary card for an 'about' page
# when configured to generated large summary cards for all non-list pages,
# enter the path to the about page and the small summary card value: 'summary'
#
# overrides data/plugin_cards/twitterog/twitter/card/assignments.toml
##############################################################################
[TwitterOG.Twitter.Card.Assignments]
'/about/' = 'summary'
'/gallery/' = 'summary_large_image'
'/cloud/' = 'summary'
'/bookshelf/' = 'summary'
# Structured Data Parameters
#
# overrides data/plugin_cards/structureddata/config.toml
[StructuredData.Config]
# Whether to inject an application/ld+json script in the page <head>
# with structured data for search engines.
#
# Enable = true
# The value to set for author.name within the JSON object.
# Defaults to site.Author.name or site.Params.Author.name when empty
#
# AuthorName = ''
# The value to set for author.url within the JSON object.
# Defaults to site.Author.profileurl when empty
#
# ProfileURL = ''
# Parameters for controlling the generated css and js files
# and preview card creation.
# overrides data/plugin_cards/Cardify/Config.toml
###########################################################
[Cardify.Config]
# Whether to provide subresource integrity by generating a
# base64-encoded cryptographic hash and attaching a .Data.Integrity
# property containing an integrity string, which is made up of the
# name of the hash function, one hyphen and the base64-encoded hash sum.
#
# Fingerprint = true
# Output style for /assets/sass/cardify.scss.
# Valid options are nested, expanded, compact and compressed
#
SassOutput = 'compressed'
# Parameters for styling preview cards
# overrides data/plugin_cards/Cardify/Style.toml
######################################
[Cardify.Style]
# Sass block to apply to the card container.
#
# Card = '''
# background-color: white;
# border-color: rgba(black, .125);
# border-style: solid;'''
# Sass block to apply to the card body.
#
# Body = 'padding: 1rem;'
# Sass block to apply to the card title.
#
# Title = 'margin-bottom: .5rem;'
# Sass block to apply to the card text.
#
# Text = ''
# Sass block to apply to the reading time element.
#
# ReadingTime = 'color: #9091AB;'
# Sass block to apply to the publish date element.
#
# PublishDate = 'color: #666;'
[Cardify.Style.Variables]
# Width value for the border applied to card container.
#
# BorderWidth = '1px'
# border-radius value for the card container.
#
# BorderRadius = '.5rem'
-
Now I kinda want to make Learn at your own discretion stamp image I could plop on top of such statements. ↩︎
-
This is why the default og:type for audio is music.song. ↩︎
-
Fancy for files located in the
data
directory. ↩︎ -
I was gonna give the JSON site the award for least helpful landing page introducing a markup language… ↩︎
-
but then I saw the YAML landing page. On top of being entirely unhelpful, YAML also loses all the points it originally earned for naming itself Yet Another Markup Language (a likely nod to the coolness of YACC) on account of its apparently rebranding itself YAML Ain’t Markup Language (which is both stupid AF and recursively incorrect). ↩︎
-
YAML files do not. ↩︎
-
‘Cause how often do we add or remove values when storing configuration data? Seriously, how is JSON in the least bit fit as a configuration file format? ↩︎