Automating Blog Tagging with AI
Tagging is a Pain
Tagging blog posts is one of those small things that nags at you more than it should.
- You have to keep things consistent with existing tags
- When you add a new tag, you want the granularity to match (genre? theme? specific tech name?)
- You need to decide how to label each tag per language
- The more posts you have, the harder it gets to reconcile new tags with old ones
"Classification by rules" is exactly where AI shines. So I built a script that uses the Gemini API to automate tagging, and then wired it into a GitHub Actions pipeline for full automation.
tags.ts: The AI Tagging Script
I started with a script that runs locally. What it does is simple: hand the body of a Japanese post plus the existing tag list to Gemini, and let it suggest and apply appropriate tags.
That said, letting AI invent tags freely is a recipe for chaos, so I baked in a few rules.
Tag Granularity Rules
Tags have three levels of granularity, and I feed the whole system to the AI as context.
| Granularity | Meaning | Examples |
|---|---|---|
large | Genre / category | blog, tech, entertainment |
medium | Field / theme | ai, web-development, anime |
small | Specific tech / work titles | nextjs, vercel, supabase |
Minimum 2 tags per post, max 10. Having at least one tag from each granularity is a "best effort" goal, not a hard rule (making it a hard rule would just inflate the tag count on short posts).
Deduplication
When you let AI freely invent tags, you get duplicates like "ai" and "machine-learning" sitting side by side. I explicitly tell it in the prompt "don't create tags that overlap conceptually with existing ones," so it only creates new tags for genuinely new topics.
Japanese Label Conventions
For Japanese labels, especially when this blog mixes in tech-heavy posts, it's tricky to decide whether to write technical terms in katakana or keep them in the alphabet. My prompt tells the model to follow whatever convention the Japanese tech community actually uses. Don't force katakana on terms that are normally left in English.
- java → "Java" (good), "ジャバ" (bad)
- embedding → "Embedding" (good), "エンベディング" (questionable)
- blog → "ブログ" (good), "Blog" (questionable)
If the initial label looks off and I want to change it, I just edit tags.json by hand — and the AI will respect that as the source of truth from then on. Human judgment always wins.
Handling LLM Output Variance
LLM output drifts a bit every time, so I added validations like checking tag counts and verifying tag IDs exist, with retries on failure. The trick is splitting rules into hard rules and best-effort goals — don't make everything strict. If you do, the tagger will fail forever and never tag anything.
Picking the Right Model
Since the free tier has limits, I switch models based on the task. For tagging I use a lightweight model suited for high-volume runs — gemini-3.1-flash-lite-preview, with its RPD of 500. The overall tag system quality review (catching duplicates, granularity mistakes, awkward labels) is something I run manually on the side — and for that I use a smarter model (gemini-2.5-flash) since it needs a more holistic view.
But here's the catch: right now gemini-3.1-flash-lite-preview gives a generous RPD 500 because it's in preview. Once preview ends, that goes away, so I'll probably have to rethink this sooner rather than later...
What Prompt Tuning Buys You
Let me show you the difference, using the same article (How I Built Automated Translation Quality Checks with AI) for the comparison.
Naive Prompt
Just telling it "please tag this article appropriately":
ai, llm, translation, backtranslation, embedding,
vectorsearch, pgvector, postgresql, nlp, automation
Problems everywhere.
- Duplicates: "ai", "llm", and "nlp" all overlap conceptually
- Duplicates: "pgvector" and "postgresql" are the same thing at different granularities
- Ignores existing tags: "backtranslation" got created separately from the existing "translation"
- Mixed granularity: Genre tags and specific names dumped together with no distinction
- Maxed out: All 10 slots used
Prompt With Rules
When I pass it the existing tag list, the granularity system, and the dedup rules:
[large] tech
[medium] ai, translation
[small] postgresql, embedding, gemini, supabase
Trimmed down to 6, no duplicates. Existing tags get priority, and new tags only show up when they're genuinely needed.
The reason for this gap is that asking AI to "classify freely" and asking it to "classify within this rule system" are completely different tasks. The latter, being constrained, gives you stable results.
Wiring It Into GitHub Actions
With tags.ts working locally, the next step is to wire it into the GitHub Actions translation pipeline.
Design: Tagging → Translation → All in One Commit
This blog already had a setup where pushing an article kicks off automatic translation (here's how the translation works). I added tagging as the step before that.
push to posts/ja/
→ detect slugs of changed articles
→ AI tagging (tags.ts apply) ← added
→ translation (translate.ts translate)
→ pick best translation (translate.ts best)
→ bundle into a single git commit & push
The key point is that tag changes and translation changes are bundled into one commit. I git add everything — posts/ja/ (updated frontmatter tags), posts/en/ (generated English), content/tags.json (new tags) — and then commit once. Splitting commits makes the history messy, so bundling matters.
This Article Is the Test
I'm pushing this article with tags: [] — no tags. If the pipeline works correctly, the AI should read the article, auto-generate appropriate tags, and write them into the frontmatter.
If you see tags on this article, the automated tagging pipeline is working.
While I'm At It: Auto-Generating meta description
With the tagging setup stable, I figured I'd auto-generate the HTML <meta name="description"> at the same time.
What Is a Meta Description, Anyway?
It's the article summary that shows up in search results or when shared on social media. It matters for SEO — Google's search results show up to about 120 Japanese characters.
Usually you either write it by hand for every post or mechanically clip the start of the body. Neither option is great. Manual is tedious, and clipping the start tends to grab things like "## Introduction" headings or cut sentences mid-thought.
One API Call, Both Tags and Description
What I did was dead simple: I just added "and while you're at it, write a 120-character summary" to the tagging prompt.
{
"tags": ["tech", "ai", "gemini", "github-actions"],
"new_tags": {},
"description": "On granularity and deduplication in blog tagging...",
"reasoning": "..."
}
API call count doesn't go up. Since I'm already feeding the full article to suggest tags, adding the summary costs nothing extra. No additional hit to the free-tier RPD either.
Respecting Manual Descriptions
If description is already filled in in the frontmatter, I don't overwrite it. Sometimes the one I wrote myself is just better, so the AI only fills in blanks.
Recap
- Built an AI tagging script (tags.ts) using the Gemini API
- Implemented granularity rules, deduplication, JA/EN label generation, and validation + retries
- Integrated it as the first step in the GitHub Actions translation pipeline, all in one commit
- Auto-generate meta description in the same API call as tagging (zero added cost)