% cd ..

Automating Blog Tagging with AI

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.

GranularityMeaningExamples
largeGenre / categoryblog, tech, entertainment
mediumField / themeai, web-development, anime
smallSpecific tech / work titlesnextjs, 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)