theme: Moondeer v2.0.1 (built on Feb 2 at 11:36)

plugin-cards (a README Experience)

Card Collage

plugin-cards is a Micro.blog plugin for generating link preview cards on various platforms (including your blog) as well as generating structured data for search engine consumption. Its code lives here.


Okay, I’m in … wait … what do I need to do … wait … what exactlydoes 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 performing a particular service:

TwitterOG
This module helps social media platforms make snazzy little preview cards when we drop in links to our site.
StructuredData
This module helps search engines create richer results for our blog pages.
Cardify
This module provides snazzy little preview cards when we drop our links into our own site.

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, StructuredData.

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.

<script type="application/ld+json">
{
  "@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
}
</script>

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 site 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 (or add a profileurl value to your site configuration's author setting); 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. 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:

Rich Results Test

Wait for it to do its thing and then it will be all:

Test Results

Expanding detected items lets us see whether it sniffed out what we left for it to find.

Detected Items

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. Let's move on to the module that configures your pages to generate preview cards when their links appear on other platforms.

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 published and modified dates, 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 above <meta> tags in place, the three platforms being considered will generate preview cards kinda like:

Facebook

Facebook

Twitter

Twitter

Messages

Messages

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:

Twitter - Summary


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. Both examples will also include the card-config shortcode, the details of which will be explained … actually … I'm not sure when TF I am actually going to give you the details.

Side note: anyone else finding it ironic just how much effort I seem to be putting into documentation meant to elucidate that does more to befuddle? F$&k it. Am I not entertained?

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 \m/,(> . <)_\m/).

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 (or an Unnecessary Side Note Masquerading as a Section of Relevance)

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).

Side note: points if you figured out that a different way was with card-config.


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

Using the shortcode (along with that flummoxingly flaunted other little bugger), the post amounts to this…

_An old original song from 2006 that I recently rediscovered_

{{< card-config image="/uploads/2022/93c0d37c47.jpg" title="That time I did the stuff and made the thing." >}}

{{< card-audio src="https://moondeer.blog/uploads/2020/b7dafae583.mp3" >}}
The HTML

The rendered HTML comes out like this:

<p><em>An old original song from 2006 that I recently rediscovered</em></p>
<!--card-config
image=/uploads/2022/93c0d37c47.jpg
title=That time I did the stuff and made the thing.
-->
<audio src="https://moondeer.blog/uploads/2020/b7dafae583.mp3?card-audio" preload="metadata" controls=""></audio>
The Tags

With the above content, the plugin will generate the following Open Graph tags…

<meta property="article:published_time" content="2020-10-09T15:17:00-08:00">
<meta property="article:modified_time" content="2022-01-31T14:26:43+00:00">
<meta property="article:section" content="2020">
<meta property="article:tag" content="Music">
<meta property="article:tag" content="Pinned">
<meta property="og:type" content="music.song">
<meta property="og:url" content="https://moondeer.blog/2020/11/26/an-old-original.html">
<meta property="og:title" content="That time I did the stuff and made the thing.">
<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="og:image" content="https://moondeer.blog/uploads/2022/93c0d37c47.jpg">
<meta property="og:audio" content="https://moondeer.blog/uploads/2020/b7dafae583.mp3">

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="That time I did the stuff and made the thing.">
<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:image" content="https://moondeer.blog/uploads/2022/93c0d37c47.jpg">
<meta name="twitter:card" content="player">
<meta name="twitter:player" content="https://moondeer.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

With the above <meta> tags in place, the three platforms being considered will generate preview cards kinda like:

Facebook (sucking the hardest)

Facebook (sucking the hardest)

Twitter (cool until you hit play and it looks like a blank f$&kin' video)

Twitter (cool until you hit play and it looks like a blank f$&kin' video)

Messages (acting f$&kin' appropriately)

Messages (acting f$&kin' appropriately)


Okay … okay … but 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 (just go with it, this descriptor was f$&king accurate for the first dozen or so drafts) card-config shortcode capable of specifying values for any of the following attributes: title, description, image, audio, video, width, height, card, type and possibly more (\#ADHD).

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).


Wait … you just f$&kin' did the same thing with the audio example. Why the hell wouldn't you just explain all this up there?

Because … f$&ker … so much time lapsed between the starting and finishing of this monstrosity that my audio example post went from not including the card-config shortcode to totally f$&king including it … and I wanted the example code to be correct more than I wanted the documentation to make sense … you know what … I don't have to explain myself to you. I could have told you earlier. I'm telling you now. Got a problem with that? Come at me bro.

(ง •̀_•́)ง

What was I saying … right …

Sooo, 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:

{{< card-config title="Wind Rider"  image="/uploads/2021/abe98ed250.jpg"  >}}
Murphy catches a breeze.

{{< card-video "https://moondeer.blog/uploads/2021/497463855c.mov" autoplay loop >}}

Side note: I am using relative paths in situations where a full URL get autolinked. Say you save the post all like image="https://moondeer.blog/uploads/2021/abe98ed250.jpg". When you reopen, it will be all all like image="[moondeer.blog/uploads/2...](https://moondeer.blog/uploads/2021/abe98ed250.jpg)". Whatevs …the plugin parses what it needs out of that sh$t regardless. My choice to use relative URLs is purely cosmetic.

The HTML

Here’s what these two little shortcodes render to HTML:

<p><!--card-config
image=/uploads/2021/abe98ed250.jpg
title=Wind Rider
-->
Murphy catches a breeze.</p>
<video src="https://moondeer.blog/uploads/2021/497463855c.mov?card-video" playsinline="" autoplay="" muted="" controls="" 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="article:published_time" content="2021-12-09T16:24:00-08:00">
<meta property="article:modified_time" content="2022-01-31T14:24:52+00:00">
<meta property="article:section" content="2021">
<meta property="article:tag" content="Critters">
<meta property="og:type" content="video.other">
<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="og:image" content="https://moondeer.blog/uploads/2021/abe98ed250.jpg">
<meta property="og:video:width" content="600">
<meta property="og:video:height" content="338">
<meta property="og:video" content="https://moondeer.blog/uploads/2021/497463855c.mov">

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:image" content="https://moondeer.blog/uploads/2021/abe98ed250.jpg">
<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">
The Payoff

With the above <meta> tags in place, the three platforms being considered will generate preview cards kinda like:

Facebook (still sucking the hardest)

Facebook (still sucking the hardest)

Twitter (now acting appropriately)

Twitter (now acting appropriately)

Messages (remaining consistent)

Messages (remaining 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 in that all-in-one 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…

About Card Without Image

then you’ll want to figure out a way to provide images for non-post pages…

About Card With ImageHomepage CardBookshelf CardCritters CardPerspectives CardPoetry CardGallery CardInside the Art Card

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, if I want to link to one of my essays in a new post … instead of lousy link … I get myself something like this:

On the American Upside Down

While the beltway press, the pundits, the influencers, the organizers, and the cogs that compose the political machinery at large desperately cling to those norms and precedents with which order has …

3:24 PM • Sep 28, 2021reading time 8 minutes

Thanks to a tip from @sod, the plugin is scrapin' pages all server-side-static … so any link to page with Open Graph or Twitter Card <meta> tags can be cardified:

GitHub - moonbuck/plugin-cards

Contribute to moonbuck/plugin-cards development by creating an account on GitHub.

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, not sure why I'd have you, the reader, say something like that. I mean … you wouldn’t … would you … say something like that? Let's try this again.)

Interesting … I mean … mildly. What feat fit for Heracles must I endeavor to complete for this to work?

Wow … sweet reference … daddy like. And the answer, my friend, is not-a-one. A simple shortcode is all it takes.

The card Shortcode

All it took to drop in that preview card for the repository was the following line…

{{< card "https://github.com/moonbuck/plugin-cards" >}}

and the shortcode spits out this HTML:

<div class="cardify-card type-summary"><img src="https://opengraph.githubassets.com/2d25518c0ae68566bc310945a38bd6c60d2f64ca681df9c3187232c96c26a530/moonbuck/plugin-cards"><div><h3>GitHub - moonbuck/plugin-cards</h3><p>Contribute to moonbuck/plugin-cards development by creating an account on GitHub.</p><a href="https://github.com/moonbuck/plugin-cards"></a></div></div> 

The horizontal card up there was created by including this line…

{{< card "https://moondeer.blog/perspectives/" horizontal >}}

and the shortcode spits out this HTML:

<div class="cardify-card type-summary horizontal"><img src="https://moondeer.blog/uploads/2021/f5f64b49bb.jpg"><div><h3>Perspectives</h3><p>Essays off the beaten path.</p><a href="https://moondeer.blog/perspectives/"></a></div></div>

Okay, so … which one of y'all has spotted the catch-22 we just created? One of y'all must have.


*crickets*

None of ya, huh? No worries. It took me a half-step to realize the dilemma … and another half-step to sort it out. Let's have a look, shall we?

The Catch-22

With the TwitterOG module, we figured out how to ensure that when our links showed up on other platforms, they were all dressed up and looking sharp.

With the Cardify module, we capitalized on the data we embedded by scraping it up for ourselves to dress up our links (and any suitably marked up link) within our own blog pages.

Now, if the content of your post is greater than 280 characters … you're golden. @manton is gonna chop off your content and drop in a link before cross-posting.

What if the post, despite the preview card, is less than 280 characters? What if the whole point is to get the post to generate that preview card on other platforms? What if you also want the link to generate a preview card within the original post on your site? Creating the card on your site boinks card generation on every other platform because that requires a plain old link in the content. You are sh$t outta luck, my friend.

At least, you would be if I hadn't handled that sh$t. If you want your link to crossover before being cardified, you must add vertical or horizontal (to keep the positional parameters predictable) and then add cross like so…

{{< card "https://moondeer.blog/2021/09/07/on-the-american.html" vertical cross >}}

and the shortcode spits out the following HTML:

<a class="cardify" href="https://moondeer.blog/2021/09/07/on-the-american.html">https://moondeer.blog/2021/09/07/on-the-american.html<!--<div class="cardify-card type-summary"><img src="https://moondeer.blog/uploads/2021/cbeaf7bb2f.jpg"><div><h3>On the American Upside Down</h3><p>While the beltway press, the pundits, the influencers, the organizers, and the cogs that compose the political machinery at large desperately cling to those norms and precedents with which order has …</p><a href="https://moondeer.blog/2021/09/07/on-the-american.html"></a><p><small>3:24 PM • Sep 28, 2021</small><small>reading time 8 minutes</small></p></div></div>--></a>

The RSS feed spits out the link that makes all the other platforms happy … and running a little Javascript once the DOM loads makes our blogs happy:

// Fetch and process any eligible links with the query parameter.
  document.querySelectorAll('a.cardify').forEach(link => {
    link.outerHTML = link.childNodes[1].textContent;
    })

Alright, how's everybody doin?


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 should you wish to override the default card style:

# 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 = ''

# Sass block to apply to the card body.
#
Body = ''

# Sass block to apply to the card title.
#
Title = ''

# Sass block to apply to the card text.
#
Text = ''

# Sass block to apply to the reading time element.
#
ReadingTime = ''

# Sass block to apply to the publish date element.
#
PublishDate = ''

# Values that appear in multiple places within the Sass template file.
[Variables]

# Width value for the border applied to card container.
#
BorderWidth = '1px'

# border-radius value for the card container.
#
BorderRadius = '.5rem'

Any valid CSS is also valid SCSS flavored Sass. These parameter values are then injected into the Sass file template used to generate the plugin’s stylesheet like so:

@import "vendor/rfs";

// Plugin parameter values
$border-width:         1px;
$border-radius:        .5rem;
$inner-border-radius:  calc(#{$border-radius} - #{$border-width});

// The card container
.cardify-card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  word-wrap: break-word;
  background-clip: border-box;
  background-color: white;
  border-color: rgba(black, .125);
  border-style: solid;  
  border-width: $border-width;
  border-radius: $border-radius;
  /* Style.Card */

  // The card image
  img {
    width: 100%;
    aspect-ratio: 1200 / 628;
    object-fit: cover;
    padding: 0;
    margin: 0;
    border-top-left-radius: $inner-border-radius;
    border-top-right-radius: $inner-border-radius;
  }
  
  // The card video
  video {
    width: 100%;
    aspect-ratio: 1200 / 628;
    object-fit: cover;
    padding: 0;
    margin: 0;
    border-top-left-radius: $inner-border-radius;
    border-top-right-radius: $inner-border-radius;    
  }
  
  audio {
    width: 100%;
    position: relative;
    top: -24px;
  }
  
  // The card body containing the title, description, 
  // date and reading time.
  div {
    flex: 1 1 auto;
    [@include](https://micro.blog/include) margin(.125rem .25rem);
    [@include](https://micro.blog/include) padding(1rem);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-content: center;
    gap: .25rem;
    /* Style.Body */
    
    // The card title
    h3 {
      padding: 0;
      [@include](https://micro.blog/include) margin(0 0 .5rem 0);
      [@include](https://micro.blog/include) font-size(1rem);
      line-height: 1.1;
      /* Style.Title */
    }

    // The remaining card text (description and time/date/reading-time) 
    p {    
      padding: 0;
      margin: 0;
      line-height: 1;
      [@include](https://micro.blog/include) font-size(.9rem);
      
      /// Just the description
      &:not(&:last-child) {
        /* Style.Text */
      }
      
      // Just the time/date/reading-time
      &:last-child {
        display: flex;
        justify-content: space-between;
        align-content: stretch;
        [@include](https://micro.blog/include) margin-top(.75em);
        [@include](https://micro.blog/include) padding-top(.75em);
        border-top-width: $border-width;
        border-top-color: rgba(black, .125);
        border-top-style: solid;
        
        &:empty { display: none; }       
      }
      
      // The publish date.
      small:first-child {
        color: #666;
        /* Style.PublishDate */
      }
      
    } // p
        
  } // div
  
  &.type-summary div {
    
    // Stretching the invisble link across the entire card.
    a::after {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      z-index: 1;
      content: "";
    }
    
    // The reading time.
    p small:last-child {
      color: #9091AB;
      /* Style.ReadingTime */
    }  
    
  }

  &.type-video div {
  
    // Stretching the invisble link across the entire card.
    a::after {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      z-index: 1;
      content: "";
    }
      
  }
  
  &.type-audio div {
    position: relative; 

    // Stretching the invisble link across the entire card.
    a::after {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      z-index: 1;
      content: "";
    }
    
  }
  
} // .Style.ClassName

// Overrides for horizontal cards
.cardify-card.horizontal {
  flex-direction: row;

  // The card image
  img {
    width: 30%;
    aspect-ratio: 1 / 1;
    height: auto;
    object-fit: cover;
    border-top-right-radius: 0;
    border-bottom-left-radius: $inner-border-radius;
  }
  
  // The card video
  video {
    width: 30%;
    aspect-ratio: 1 / 1;
    height: auto;
    object-fit: cover;
    border-top-right-radius: 0;
    border-bottom-left-radius: $inner-border-radius;      
  }
  
  // The card title
  h3 { @media (max-width: 600px) { text-align: center; } }

  // The remaining card text (description and time/date/reading-time) 
  p {    
    
    // Just the description
    &:not(&:last-child) { @media (max-width: 600px) { display: none; } }
    
    // Just the time/date/reading-time
    &:last-child { 
      @media (max-width: 600px) { justify-content: center; } 
    }
            
  } // p
    
  &.type-summary p {
    
    // The reading time.
    small:last-child { @media (max-width: 600px) { display: none; } }
    
  }
  
  &.type-audio audio {
    width: 100px;
    position: absolute;
    left: calc(15% - 25px);
    top: calc(50% - 25px);
  }
} // .Style.ClassName

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

If you want your configuration to persist between plugin updates, we're gonna need to set you up with some configuartion files that live outside the plugin's directory. If you have (or are willing to create a custom theme), the files can live there (like mine do). If, for whatever reason, creating a custom theme seems a bit much, you can create a new plugin for the purpose of holding the configuration files. Hugo doesn't care where the files live, all your directories will be merge by path. It is for this reason that when we move our configuration files outside of the plugin we will modify the name just a smidge. Instead of using data/plugin_cards we will use data/plugin-cards.

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 the file in my plugin directory looks exactly like your file. The real file I maintain lives inside my custom theme (turns out that, as of the very latest draft of this beast, this is another blatant f$&king lie that serves a purpose). 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 = ''

# Sass block to apply to the card body.
#
# Body = ''

# Sass block to apply to the card title.
#
# Title = ''

# Sass block to apply to the card text.
#
# Text = ''

# Sass block to apply to the reading time element.
#
# ReadingTime = ''

# Sass block to apply to the publish date element.
#
# PublishDate = ''

[Cardify.Style.Variables]

# Width value for the border applied to card container.
#
# BorderWidth = '1px'

# border-radius value for the card container.
#
# BorderRadius = '.5rem'

The plugin will merge the dash-cased data file with the underscored data files, so you only really need to include what you want to change. I like to stick it all there and then comment out whatever I'm not changing by sticking a hashtag in front of it.

The Actual Actual Content of My Files

When considering whether assembling all the files into one giant file is unnecessarily tricky, I realized that Hugo could give a f$&k whether I had one giant file at data/plugin-cards.toml or I had a subdirectory identical to the plugin's data/plugin_cards directory … only modified to be data/plugin-cards. The maps produced by Hugo are identical. Just to double check, I disassembled the single file in my custom theme into the slew of files the plugin ships with like so:

Totally works … and these … finally … are my actual f$&king files. No lie.

An Alternative to Custom Theme Storage

To simplify things a bit for folks that may be intimidated by the idea of creating a custom theme, I created a plugin whose sole purpose is to house configuration files for my plugins. If you don't want to create data files in a custom theme, you can install plugin-configuration-files and modify those files instead.


  1. Now I kinda want to make Learn at your own discretion stamp image I could plop on top of such statements. ↩︎

  2. This is why the default og:type for audio is music.song↩︎

  3. Fancy for files located in the data directory. ↩︎

  4. I was gonna give the JSON site the award for least helpful landing page introducing a markup language… ↩︎

  5. 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). ↩︎

  6. YAML files do not. ↩︎

  7. ‘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? ↩︎