I built a mini homemade CMS for my Astro blog (more on this later) and ran into a niche but annoying problem: when I add tags to posts, I don’t have a consolidated list to pick from.

Instead, I have to check my blog to see what I’ve already used or decide to create new ones. It’s tedious, time consuming, and a missed chance to branch out into other useful tags. To complicate things, I also use Raindrop for bookmarks, highlights, and quotes, and it has its own set of tags. So I set out to make tagging more streamlined and painless in my CMS.


Here’s what I did:

  • Asked ChatGPT to prototype three clever UI solutions for the problem:
I need help brainstorming a solution for my blog CMS app. I can add tags to my posts and already have existing tags in my files. I also use Raindrop to import links and highlights, which come with their own set of tags. Conceptually, how could I make it user-friendly to add or remove tags from a post, while also allowing me to pick from a list of tags that shows whether each tag exists in my existing posts, in Raindrop, or in both? The goal is to maintain a consistent set of tags across the board. Feel free to ask me clarifying questions if needed.
  • Gave it feedback and iterated until I had a solid component.
  • Copied the HTML and CSS ChatGPT generated and dropped those into Cursor to implement in my CMS.

blog tags component

Then I realized I could go further: what if an LLM could suggest tags based on the title and content of a post? I went back to ChatGPT (5 Thinking) to brainstorm approaches. It proposed three, and I went with the simplest, a Closed-Vocabulary Tag Suggester. I had it generate model-agnostic specs so it would work no matter which model I used.

# Closed-Vocabulary Tag Suggester – Concise Spec

## Goal

Suggest tags for blog posts from a fixed vocabulary, using an LLM (e.g., Mistral Small, GPT‑5 Nano). Ensure consistency, easy integration, and safe outputs.

## Core Workflow

1. **Input**: Post text + list of allowed tags (closed vocabulary).
2. **Prompt**: Ask LLM to return only tags from the list, in strict JSON.
3. **Validation**: Parse + check tags are in vocabulary.
4. **Retry/Repair**: If invalid, reprompt with stricter instructions.

## Prompt Contract

* **System**: "You are a tagging assistant. Only output JSON matching the schema."
* **User**: Provide content + closed vocabulary list.
* **Output Schema**: `{ "tags": ["string"] }`

## Implementation

* Preselect relevant tags with keyword or embedding search (optional speed-up).
* Call LLM with prompt + vocabulary.
* Validate JSON and tag membership.
* Return final tag list.

## Example

Input: "Post about async Python code and coroutines."
Vocabulary: \["python", "async", "javascript"]

LLM Output: `{ "tags": ["python", "async"] }`

Next, I hopped into Cursor with Claude Sonnet 4 to build the feature from those specs. A couple of cosmetic tweaks later, it was basically perfect.

blog tags component 2

What I learned from this exercise:

  • Focusing on a tiny, specific problem gave better results than chasing big feature builds.
  • Using LLMs to brainstorm UI and UX before coding was much more efficient than doing it straight in Cursor. Context confusion there often led to half-baked ideas.
  • Prototyping designs with ChatGPT is fun. It’s fast, usually spot on, and the components often look sharp and modern out of the box.

This project was loosely inspired by Simon Willison’s tool for bulk-applying tags to old content on his blog.