8.8 KiB

jam-my-stack 🥞

A set of simple IndieWeb Jamstack publishing syndication tools

Published at https://www.npmjs.com/package/jam-my-stack

npm version

These simple scripts enrich your Jamstack-site by adding/manipulating/whatever (meta)data, such as extra posts, indexing, and so forth. A primary example of these tools in action is my own site https://brainbaking.com - inspect how it's used at https://git.brainbaking.com/wgroeneveld/brainbaking

Are you looking for a way to receive webmentions? See https://git.brainbaking.com/wgroeneveld/go-jamming !

The tools


  1. yarn add jam-my-stack
  2. const { mastodon, goodreads } = require('jam-my-stack')

1. Mastodon

1.1 parseFeed

An async function that parses your Fediverse-compatible feed (Mastodon/Pleroma/...) and converts entries to .md Markdown files for your Jamstack to enjoy.

Usage example:

    await mastodon.parseFeed({
        notesdir: `${__dirname}/content/notes`,
        url: "https://chat.brainbaking.com/users/wouter/feed",
        utcOffset: 60,
        titleCount: 50,
        titlePrefix: "Note: "

Options and their default values:

  • utcOffset: 60 (= GMT+1, that's where I am!) (in minutes, see day.js docs
  • titleCount: 50. Will add "..." and trim if title length bigger.
  • titlePrefix: "". Will add before title (e.g. "Note: ")
  • ignoreReplies: false. If true, will not process in-reply-to items.

Note that this does not delete the notes dir with every call. It simply checks if there isn't already a file with the same name (based on the publication date), and adds one if not.

Example feed entry:

  <title>I pulled the Google plug and installed LineageOS: https://brainbaking.com/post/2021/03/getting-ri...</title>
  <content type="html">I pulled the Google plug and installed LineageOS: &lt;a href=&quot;https://brainbaking.com/post/2021/03/getting-rid-of-tracking-using-lineageos/&quot; rel=&quot;ugc&quot;&gt;https://brainbaking.com/post/2021/03/getting-rid-of-tracking-using-lineageos/&lt;/a&gt; Very impressed so far! Also rely on my own CalDAV server to replace GCalendar. Any others here running &lt;a class=&quot;hashtag&quot; data-tag=&quot;lineageos&quot; href=&quot;https://chat.brainbaking.com/tag/lineageos&quot; rel=&quot;tag ugc&quot;&gt;#lineageos&lt;/a&gt; for privacy reasons?</content>
  <ostatus:conversation ref="https://chat.brainbaking.com/contexts/ff9aa62e-3357-41ad-951d-15f6ad506424">
  <link href="https://chat.brainbaking.com/contexts/ff9aa62e-3357-41ad-951d-15f6ad506424" rel="ostatus:conversation"/>
    <link type="application/atom+xml" href='https://chat.brainbaking.com/objects/77a3ecfb-47e1-4d7a-a24a-8b779d80a8ac' rel="self"/>
    <link type="text/html" href='https://chat.brainbaking.com/objects/77a3ecfb-47e1-4d7a-a24a-8b779d80a8ac' rel="alternate"/>
    <category term="lineageos"></category>
      <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://chat.brainbaking.com/users/wouter"/>

This generates the file 01h20m03s35.md (it assumes UTC times in the feed and adjusts according to specified utcOffset, such as GMT+1 in this example), with contents:

source: "https://chat.brainbaking.com/objects/77a3ecfb-47e1-4d7a-a24a-8b779d80a8ac"
title: "I pulled the Google plug and installed LineageOS: https://brainbaking.com/post/2021/03/getting-ri..."
date: "2021-03-01T19:03:35"

I pulled the Google plug and installed LineageOS: <a href="https://brainbaking.com/post/2021/03/getting-rid-of-tracking-using-lineageos/" rel="ugc">https://brainbaking.com/post/2021/03/getting-rid-of-tracking-using-lineageos/</a> Very impressed so far! Also rely on my own CalDAV server to replace GCalendar. Any others here running <a class="hashtag" data-tag="lineageos" href="https://chat.brainbaking.com/tag/lineageos" rel="tag ugc">#lineageos</a> for privacy reasons?

See implementation for more details and features.

Also parsers:

  • <link rel="enclosure"/> image types (see render-enclosures.ejs) ejs template, that is appended to the Markdown file if any are found. Styling is up to you...
  • ... @https://blah.com/blie hi there - this is a in-reply-to toot which adds context frontmatter, so your html renderer can use the correct IndieWeb classes. This should also enable webmention sending since you mention the URL. If you "at" a valid Mastodon user, it will automatically do this.

2. Goodreads

2.1 createWidget

An async function that reads and modifies Goodreads JS widget embed code, converting low-res book covers to hi-res ones if possible. This omits possible Goodread cookies and cross-domain mishaps.

Usage example:

    const widget = await goodreads.createWidget("https://www.goodreads.com/review/grid_widget/5451893.Wouter's%20bookshelf:%20read?cover_size=medium&hide_link=&hide_title=&num_books=12&order=d&shelf=read&sort=date_added&widget_id=1496758344")
    await fsp.writeFile(`${__dirname}/static/js/goodreads.js`, widget, 'utf-8')

3. Lunr

As of version 1.0.30, Lunr functionality was removed in favor of https://pagefind.app/

With Pagefind, there's no need to integrate it into jam-my-stack, greatly simplifying things and reducing the index file size.

4. Howlongtobeat

4.1 howlong

Adds https://howlongtobeat.com/ game length (MainGame) and an ID to your front matter (keys howlongtobeat_id and howlongtobeat_hrs), provided you first added a property called game_name. (This gets substituted).

It also downloads a thumbnail of the cover image as cover.jpg in the same relative directory as the source article if you provided the dir as an option. The downloaded thumbnail is automatically optimized for the web using mogrify (this will emit a warning if you do not have ImageMagick installed locally).

So, Frontmatter like this:

title: Diablo 3 my Review
game_name: Diablo 3

Gets subsituted by something like this:

title: Diablo 3 my Review
howlongtobeat_id: 62129
howlongtobeat_hrs: 20.5

In your Hugo template, add a link to https://howlongtobeat.com/game?id={howlongtobeat_id} and you're all set!

Usage example:

  await howlong({
    postDir: `${__dirname}/content`,
    downloadDir: `${__dirname}/static`)

It will print out games and metadata it found.

Suppose the above diablo-3.md lives in content/games/switch/diablo-3, then a cover.jpg will be automatically downloaded in static/games/switch/diablo-3/ and that directory will be created if not yet existing.

Working example: https://jefklakscodex.com/games/diablo-3/ (on the left side). Check out the Hugo template to use the properties at https://git.brainbaking.com/wgroeneveld/jefklakscodex.

5. Webmentions

In cooperation with https://git.brainbaking.com/wgroeneveld/go-jamming

5.1 getWebmentions

Calls the get webmention endpoint, sorts by date, adds metadata such as relative date (relativeTarget, property), and returns data. Could be written in a data folder for Hugo to parse, for example.

Parameters: first domain, second the config for the endpoint and token. Usage example:

await getWebmentions("brainbaking.com", {
  endpoint: 'https://jam.brainbaking.com',
  token: 'lol'

5.1 send

Calls the set webmention endpoint using a PUT. Based on the RSS feed, see the go-jamming README.

Same as getWebmentions.

6. YouTube

Thanks to ideas from rubenerd.com and his video.sh script. This downloads a thumbnail using youtube-dl, smacks a play button on it using convert, and stores that in the specified folder. Use in conjunction with a Hugo shortcode to get rid of YouTube's iframes!

This method will fail if you do not have ImageMagick installed locally.

Usage example:

  await download({
    postDir: 'somewhere/posts',
    downloadDir: 'static/youtube-thumbs',
    overlayImg: 'playbtn.png'

It scans all .md files in the posts dir for {{< youtube xxx >}} shortcodes.