Home Button

/

posts

/

dynamic-badges

/

Me

Button to go back to home page

Joshua Clements

Dynamic Badge Generation

06/01/2024

Using Sharp to create 7000 badge variants at build time to support resizing and multiple formats

Table of Contents


Over the Christmas period I had the urge to rebuild and improve something.

This happens — unfortunately — often.

The result of this urge was Fancy Badges. I forked Devin’s Badges (which is, at the time of writing, unmaintained) and decided to have a crack rewriting the technology behind it.

Context

Everyone knows ReadMe badges. Ones such as those generated by Shields.io. They do a great job at providing clearly visible dynamic data.

Example of a github status badge
Example badge generated by GitHub for one of my projects

However they have a problem. While useful for developers at a glance, they aren’t ideal for user-facing documentation.

This is what Devin’s Badges aimed to solve — nicer looking badges to accompany documentation for non-technical users. I had lots of cool ideas for badges to add1.

Given it’s unmaintained status I decided it would be a fun idea to port over the badges to a new backend and while I’m at it have a go at adding my own badges and adding some new features.

What I expected to be a weekend task turned into a week and a half descent into madness as I ended up doubling the badges from 153 to 300 (where I promptly decided to call it for now)2

However, as much as some of the new badges are cool, arguably the most useful feature was the dynamic image generation (performed at build time)

The Problem

The original project provided badges in 2 different formats:

  • SVG — the preferred choice given their scalability
  • PNG — as an alternative for places that don’t support SVG

However this has a few caveats. The main one being the issue of sizing. Fancy Badges is statically generated in advance, for performance reasons, and so while having the option for the user to resize may not be possible, it comes with significant benefits from a DX perspective.

If I want to change the height of one of my badge types for legibility purposes, I would be forced to go through every single one and re-export them. This process is incredibly laborious (I can attest) and takes hours.

Also, if I wanted to offer my badges in another format, I once again have to go and re-export each one.

Statically Generating Paths

I decided to use Astro to build the website, with the aim of shipping minimal JS to the client. This holds true — only a single file is shipped to the file and that is the theme switcher.

Typically, when using SSR, Astro doesn’t know about what your endpoints could link to, and so requires a server to respond dynamically.

Astro allows you to export a function called getStaticPaths which contains every single permutation of the paths available. With this, it can generate every path during the build step to avoid the need for a server, sweet!

export function getStaticPaths() {
// ...
}

And thus I stole built a function to generate all the permutations of a badge’s name, size and format

export function permutate(list: PermutedArray, n = 0, result: PermutedArray = [], current: PermutedArray[number] = []){
if (n === list.length) result.push(current)
else list[n]!.forEach((item: any) => permutate(list, n+1, result, [...current, item]))
return result
}

Sharp

Sharp is a library designed to perform image processing3. By passing in the raw SVG file with the badge, I can perform a bunch of operations to transform and convert the file.

With this I can resize my images to my heart’s content. While I was at it I decided to also make it support exporting the badges in the following formats:

  • JPEG
  • WEBP
  • AVIF

And with these extra formats the total number of paths bumped up to just… 6684 — oh. That shouldn’t take ages to build with all the image compression steps I’ve added on, right?

The cloudflare pages dashboard, showing the build time of 12m3s

Why at Build Time?

There was a clear tradeoff to make here.

I could prioritise build time, at the expense of runtime performance — or I could prioritise runtime performance at the expense of build time.

I chose the former. For each minute I save in building multiple hours would be saved in image generation time. My piffy little site gets updated meaningfully once a week at most. The tradeoff doesn’t advantage me here.

Ultimately, it’s cheaper and easier to run (since it’s only dealing with raw HTML, CSS and JS).

Check It Out For Yourself!

If this sounds interesting to you please check out the website over at badges.penpow.dev.

If you have any badge ideas or other feature ideas open an issue, and if you want to help contribute you can read the contributing guide.

Footnotes

  1. The raw and unabridged todo file is unfortunately lost to time, however at one point it had around 300 lines of ‘in progress’ ideas

  2. I also decided to adjust the folder structure moving from the old system of [size]/[category]/ (with every file dumped in this folder) to [category]/[badge]/[size].svg


    This alone took up multiple days of my life

  3. My favourite function it provides is flop purely based on the name