I recently decided to give my blog a visual refresh. While I loved the content, the thumbnails were a mix of generic gradients and random screenshots. I wanted something that felt cohesive, technical, and aligned with the "terminal/cyberpunk" aesthetic of my portfolio.
Instead of manually designing a cover image for every new post in Figma or Canva, I did what any engineer would do: I wrote a script to do it for me.
The Goal
I wanted thumbnails that looked like a terminal window executing a command related to the post. For example:
- A backend post might show a
curlrequest. - A personal post might show a
cat journal.mdcommand. - A database post might show a
psqlquery.
The result needed to be a high-quality PNG that could be used as an Open Graph image (for Twitter/LinkedIn cards) and as the header image on the blog itself.
The Tech Stack
- Node.js: The runtime for the script.
- Puppeteer: A headless Chrome Node.js API. This is the secret sauce. It allows me to render HTML/CSS exactly as a browser would and take a screenshot.
- HTML/CSS: To design the "terminal window" layout.
The Implementation
1. The Template
First, I created an HTML template (template.html) that defines the look of the terminal. It uses Flexbox to center a "window" div, and CSS to style the window controls (the red, yellow, and green dots) and the dark background.
I used Google Fonts to load JetBrains Mono for the terminal text and Outfit for the window title, ensuring typography matches the rest of my site.
<div class="terminal-window"> <div class="window-header"> <div class="window-controls"> <!-- Mac-style dots --> <div class="control-dot red"></div> <div class="control-dot yellow"></div> <div class="control-dot green"></div> </div> <div class="window-title">~/blog/posts/{{windowTitle}}</div> </div> <div class="terminal-content"> {{terminalContent}} </div> </div>2. The Generator Script
The script (generate.js) reads my blog's configuration and post metadata. It then launches a headless browser instance using Puppeteer.
For each post, it:
- Determines the "primary tag" (e.g.,
backend,personal,frontend). - Selects a corresponding "command pattern" based on that tag.
- Injects the post title and the generated command into the HTML template.
- Takes a screenshot of the rendered page.
- Saves the image to
public/assets/thumbnails/.
Here's a snippet of the logic that maps tags to terminal commands:
switch (primaryCategory) { case 'backend': commandHtml = `curl -X POST /api/v1/deploy`; outputHtml = `{"status": "success", "message": "Deployment initiated..."}`; break; case 'personal': commandHtml = `cat ~/journal/${slug}.md`; outputHtml = `Reflecting on: "${description}"`; break; // ... other categories }3. Build Integration
The best part is that this is now part of my build pipeline. Whenever I run npm run build:blog, the script checks if a thumbnail exists for each post. If it's missing, it automatically generates a fresh one in milliseconds.
This means I never have to worry about assets again. I just write the markdown, tag it appropriately, and the system generates a beautiful, custom thumbnail that perfectly matches the content.
The Result
You're looking at it right now! The thumbnail for this post was generated by the very system it describes. It's a meta-example of automation serving creativity.
By treating design assets as code, I've ensured 100% consistency across the blog while removing manual toil from my writing process.