Building a Toolbox for My Website
A little while ago, starting this article would have involved rather more manual work. It didn’t occur to me to write a tools for this. After all, how much friction can there be in creating a new Markdown (well, MDX) file when I want to write an article? Or adding a new component to the sites UI system? Or scan for images I intended to use but were left unused?
Well, more friction than I would have liked! Even a small amount of friction, with enough repetition, is annoying. Which is where the “toolbox” comes in. I think it’s a pretty nifty name, if overused. This toolbox primarily makes four things easier:
- Discoverability. I already had some small scripts, but remembering their names and how to invoke them was starting to grate on me. This is poor developer experience.
- Standardisation. The scripts I already had had grown pretty organically, as they tend to do. One script would create or update a file differently from another script. Or it would handle command-line arguments differently from the next one, etc.
- Automation. Creating a new article or note, creating a new component with documentation and tests, and creating a new branch with the same name as an article are now all one command away. This also helps with consistency.
- Interactivity. Some of the tools are much nicer to use with prompts and choices, like selecting an article from a list, choosing from existing tags, or entering the title of a new article and having it converted to AP-style title case.
I realised I would need both a task runner and a templating tool. The task runner would help with discoverability, while the templating tool would handle generating or updating files consistently. I was a little concerned that solving my problems with two tools might introduce more friction than I was trying to remove in the first place. As it turns out, I am very happy with these tools and my concerns were unfounded.
It had been some time since I last looked at the templating tools available for JavaScript, and I found much the same malaise as when I last looked into this a few years ago. The whole area feels oddly stagnant.1 I chose Plop which I’m fairly happy with. For the rask runner, I evaluated two options.
- Task. This was the first one I tried, as it seems to be popular. I was happy with it, and its use of YAML, controversial as that may be, was actually a plus for me. I don’t particularly enjoy using bespoke DSLs unless they have good tooling and editor support.
- Just. I switched to Just almost immediately after reading a Hacker News comment. Just will pass along extra command line arguments. It does have a downside of once again being a bespoke DSL, vaguely inspired by makefiles. Still better than Terraform with its psuedo-JSON.
Overview#
My “justfiles” are organised into modules. Each one fulfils a particular role. There is some overlap, such as article having commands related to updating an article, while new having commands to create a new article or note. I suppose I could instead have one file.
Just is just the command runner in this setup, and it invokes Plop. Plop is a template-based generator for creating new files, but it can also edit existing files. Such as, updating fields in files like YAML frontmatter in a Markdown file. The templates are a typical Handlebars syntax.
lloydatkinson.net├── justfile├── src└── toolbox ├── article.just ├── deploy.just ├── format.just ├── image.just ├── links.just ├── new.just ├── plopfile.ts ├── plop-templates │ ├── article.hbs │ ├── component.docs.hbs │ ├── component.hbs │ ├── component.snippet.hbs │ ├── component.test.hbs │ ├── note.hbs │ └── page.hbs ├── test.just └── ui.justWriting content#
The first tools I built automated the creation of articles, notes, and pages. Articles and notes are MDX files, while a page is a particular type of Astro component used for a root-level page on the site. I discuss later a command for creating UI type components used within those pages.
PS E:\Source\lloydatkinson.net> just newAvailable recipes: article # Create a new article article-branch # Create a new article on a new branch from master note # Create a new note page # Create a new page, optionally inside a new or existing directory
PS E:\Source\lloydatkinson.net> just new article-branch? Article title: Building a Toolbox for My Website? Description (optional): I built a small collection of tools to automate repetitive tasks and make working on my website easier.? Pick existing tags (optional): Astro, Automation, Static Sites, Workflows, Tools? New tags, comma-separated (optional):✔ createBranch Created and switched to branch post/building-a-toolbox-for-my-website✔ ++ /src/content/article/2026/building-a-toolbox-for-my-website.mdx✔ openInEditor Opened src/content/article/2026/building-a-toolbox-for-my-website.mdx in VS CodeThe Plop template for an article looks like this. Note the conditional and iteration constructs. The tags property always exists, but is an empty collection should none be supplied.
---title: "{{{yamlTitle}}}"description: "{{{yamlDescription}}}"dates: published: {{publishedDate}}{{#if tagList.length}}tags:{{#each tagList}} - {{{this}}}{{/each}}{{else}}tags: []{{/if}}status: draft---PS E:\Source\lloydatkinson.net> just new article-branch? Article title: Reducing .NET AWS Lambda Cold Starts by Adding Memory? Description (optional): A pleasantly simple fix that reduced cold-start time from ~7 seconds to ~2.5 seconds.? Pick existing tags (optional): .NET, AWS? New tags, comma-separated (optional): AWS Lambda✔ createBranch Created and switched to branch post/reducing-dotnet-aws-lambda-cold-starts-by-adding-memory✔ ++ /src/content/article/2026/reducing-dotnet-aws-lambda-cold-starts-by-adding-memory.mdx✔ openInEditor Opened src/content/article/2026/reducing-dotnet-aws-lambda-cold-starts-by-adding-memory.mdx in VS CodePS E:\Source\lloydatkinson.net> just new note? Pick existing tags (optional): Astro, Tools? New tags, comma-separated (optional):✔ ++ /src/content/note/24.mdx✔ openInEditor Opened src/content/note/24.mdx in VS CodePS E:\Source\lloydatkinson.net> just article status? Which article? Building a Toolbox for My Website (draft) (src/content/article/2026/_building-a-toolbox-for-my-website.mdx)? New status: published✔ setArticleStatus Set published and renamed src/content/article/2026/_building-a-toolbox-for-my-website.mdx to src/content/article/2026/building-a-toolbox-for-my-website.mdxCreating components#
All components in my site start with this recipe. This recipe assumes they are design system components, hence the ui. For other one-off, article-specific components, my current workflow is to still run this recipe and move the files to the appropriate place. This itself is an opportunity for a recipe.
Take note of the <name>.<type>.<extension> convention I’m using here. Tests, documentation, and finally documentation snippets. These are used by my design system documenation to generate example usages of components.
src/ui/components/snippets├── ButtonSolidTones.snippet.astro├── ButtonSoftTones.snippet.astro├── IconTones.snippet.astro├── StackBasic.snippet.astro├── StackAlign.snippet.astro├── TextSizes.snippet.astro├── TilesResponsive.snippet.astro└── ... 96 moreI’m quite pleased with my implementation here. I use the same snippet to generate the code examples and render real component usage in the documentation pages, which keeps the two completely in sync. The idea is that for each use case, variant, or feature of a design system component, I write a snippet file.
PS E:\Source\lloydatkinson.net> just ui component? Component name: Callout✔ ++ /src/ui/components/Callout.astro✔ ++ /src/ui/components/Callout.test.ts✔ ++ /src/ui/components/snippets/CalloutBasic.snippet.astro✔ ++ /src/ui/components/Callout.docs.astro✔ formatWithBiome Formatted src/ui/components/Callout.astro, src/ui/components/Callout.test.ts, src/ui/components/snippets/CalloutBasic.snippet.astro, src/ui/components/Callout.docs.astro✔ openInEditor Opened src/ui/components/Callout.astro, src/ui/components/Callout.test.ts, src/ui/components/snippets/CalloutBasic.snippet.astro, src/ui/components/Callout.docs.astro in VS CodeThe following files are generated:
---import Box from './Box.astro';---<Box><slot /></Box>import { experimental_AstroContainer as AstroContainer } from 'astro/container';import { describe, expect, it } from 'vitest';
import Callout from './Callout.astro';
describe('Callout', () => { it('renders', async () => { const container = await AstroContainer.create(); const result = await container.renderToString(Callout, { slots: { default: 'Some content' }, });
document.body.innerHTML = result;
expect(document.body.innerHTML).not.toBe(''); });});---import { extractSnippet } from '../foundation/snippet';
import Preview from './Preview.astro';import CalloutBasicSnippet from './snippets/CalloutBasic.snippet.astro';import CalloutBasicRaw from './snippets/CalloutBasic.snippet.astro?raw';---<Preview snippet={extractSnippet(CalloutBasicRaw)}> <CalloutBasicSnippet /></Preview>---import Callout from '../Callout.astro';---<Callout>Example content</Callout>Tidying up#
This is a group of recipes that helps me avoid forgetting to include images in an article or renaming images from the ugly names cameras tend to give them. It’s quite easy to forget to include an image on image-heavy posts, like the year-in-review posts I write.
PS E:\Source\lloydatkinson.net> just image orphaned
src/content/article/2022/images: src/content/article/2022/images/squash-merge-or-merge-commit.png src/content/article/2022/images/react-conditional-rendering.png
src/content/article/2023/images: src/content/article/2023/images/old-diagram.png
done - 3 orphaned images foundPS E:\Source\lloydatkinson.net> just image rename src/content/article/2026/imagesIMG_249234.JPG -> img-249234.jpgHEIC_42-0423.JPG -> heic-42-0423.jpg
done - 2 renamed, 0 skippedBuilds and deployments#
I don’t deploy my site manually; the Cloudflare app for GitHub handles it entirely instead of GitHub Actions, which I still use for everything else. I sometimes want to check deployment status, including preview branches and their URLs. These recipes use the Cloudflare API to do that.
PS E:\Source\lloydatkinson.net> just deploy statussuccess production master https://8569dff8.lloydatkinson-net.pages.dev (live: https://www.lloydatkinson.net)success preview post/building-a-small-toolbox- https://06548f7c.lloydatkinson-net.pages.devFootnotes#
-
I don’t want to go on a long rant about the wider ecosystem of templating tools, but Yeoman still feels overwhelming to even start using. It promotes a “generator generator” to help you generate your own generator, and its homepage still mentions Gulp and Grunt. I generally do not dismiss something simply because it reflects previously popular idioms, but the less I ever have to think again of Bower, Grunt, Gulp, Webpack, Babel, AMD, UMD, blah blah blah, the better. There are now more straightforward ways of running common project tasks and build automation. Yeoman’s documentation doesn’t seem to acknowledge that at all. Tragically, Yeoman also seems to be one of the more actively maintained options. Other previously popular tools, such as Hygen, are no longer maintained. That basically leaves Plop. It hasn’t had any commits for a few months either, but it is considerably simpler than Yeoman and, importantly, seems to work well enough. ↩