Building a Blog at Lightning Speed with Next.js + Vercel
Introduction
AI translation quality has been improving year after year, making it incredibly easy to create English versions of Japanese articles. So I decided to build a multilingual blog powered by AI. Rather than just translating articles, I wanted to make things more interesting by building in a system that automatically verifies translation quality. (The verification mechanism is covered in detail in a separate post.)
Jamstack
This blog is built with Jamstack, a relatively modern web architecture. JAM stands for:
- J (JavaScript): Frontend frameworks like React, Vue.js, and Angular handle state management and DOM operations entirely on the client side
- A (APIs): Content is fetched via APIs from headless CMS backends and other services
- M (Markup): Static site generators (SSG) such as Next.js, Hugo, and Gatsby pre-render static HTML files and deploy them to a CDN
Traditional blogs like WordPress dynamically generate HTML by querying a database every time a reader visits. Jamstack pre-renders the entire site and serves it from a CDN, achieving exceptional speed and robust security. Since all pages are generated at build time, the server simply returns pre-built HTML. Fast, and no server management required. By decoupling the frontend from the backend and database, with dynamic features handled through JavaScript and APIs, the architecture enables scalable and efficient operations.
The deciding factor was the simplicity of Vercel's workflow: "Write Markdown, push to GitHub, and it's live." Articles are managed in Git, so diffs and history are always available. No server maintenance needed — Vercel handles all the CI/CD. I initially considered using a headless CMS (Payload CMS), but with this workflow, there wasn't much reason to go through a CMS, so I decided to start with a Markdown-based approach. (Strictly speaking, this setup doesn't use APIs, so it's really just the J and M of Jamstack... but let's not worry about the details.)
Tech Stack
Here's the finalized stack. Everything can be started for free.
- Next.js 16 (App Router) — Frontend
- Markdown — Articles managed in Git with frontmatter
- Vercel — Auto build and deploy on GitHub push
- Supabase — PostgreSQL + pgvector for translation data
- Google Gemini API — Translation + embedding (free tier)
- Tailwind CSS — Styling
Design Considerations
To CMS or Not to CMS
Originally, I planned to use Payload CMS. The WordPress-like writing UI was appealing.
However, with a GitHub push → Vercel auto-deploy workflow, adding a CMS layer felt unnecessary. The simplest approach turned out to be "write Markdown, push, done."
Payload CMS is planned for a later phase, partly as a learning exercise with headless CMS. Having a working Markdown-based blog first makes it easier to appreciate the differences.
Repository Structure
Everything lives in a single private repository, dazy-blog:
- Markdown posts (
posts/ja/,posts/en/) - Next.js frontend (
src/) - Translation pipeline and tagging scripts (
scripts/) - Design documents (
docs/) - CMS configuration (
keystatic.config.ts,frontmatter.json)
A simple git push triggers Vercel's auto build and deploy. The translation pipeline only fires on changes under posts/ja/, so you can freely tinker with scripts or docs without kicking off any expensive workflows — keeping experimentation and content updates cleanly separated even within a single repository.
If you need to edit something under posts/ but want to skip the pipeline (e.g., a formatting-only cleanup commit), include [skip ci] in the commit message and GitHub Actions will skip that push entirely. The match is case-insensitive, so [Skip CI] works too. A small but surprisingly handy escape hatch.
Language Switching
Language switching uses URL prefixes.
/ja/posts/2026/04/hello-world → Japanese version
/en/posts/2026/04/hello-world → English version
Accessing / redirects to /ja. The [JA] [EN] buttons in the top-right header handle switching.
Multilingual Tags
Tags are managed by ID (lowercase English), with display names mapped per language in tags.json.
{
"tech": { "ja": "テック", "en": "Tech" },
"anime": { "ja": "アニメ", "en": "Anime" }
}
Frontmatter only contains the ID. Display names are resolved automatically based on the current language. Adding Chinese support in the future would just mean adding "zh" entries to tags.json.