Rebuilt my personal blog with Next.js 16
Building a multilingual personal blog and portfolio with Next.js 16, next-intl, and Velite — design decisions, gotchas, and the authoring workflow.
For the first post, I'll write about this blog itself: what I used, where I got stuck, and how I plan to run it.
Why rebuild
I've scattered writing across various platforms over the years. The longer I do this, the more these requirements feel non-negotiable:
- Posts should be assets I own — versioned in Git, mine forever.
- A solid SEO foundation — articles served as static HTML, full control over metadata.
- Both Japanese and English — reach the local Japanese context and the global startup context.
- Room to play on the homepage — keep the door open for 3D experimentation.
Meeting all of these means rolling your own stack. So I did.
The stack
| Area | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| i18n | next-intl 4 |
| Content | MDX + Velite (Zod schemas) |
| Styling | Tailwind CSS 4 |
| Hosting | Vercel |
| 3D slot | React Three Fiber (reserved under components/three/) |
The core principle was separating the "wow page" (homepage) from the "SEO pages" (articles). The homepage gets 3D later; the articles stay aggressively static.
Design decisions
Four fixed categories, no folder-based split
I fixed the categories at tech, product, business, essay. Any more than that and I'd freeze before writing, wondering where a post belongs.
Physically, posts are split by locale only. Categories live in frontmatter, never in folders. Changing a post's category never triggers a git mv, and a future "this post crosses two categories" scenario doesn't break the structure.
content/blog/
ja/2026-05-24-hello-world.mdx
en/2026-05-24-hello-world.mdx
Flat article URLs
Article URLs are /[locale]/blog/<slug> — no category in the path. If I reclassify a post later, the URL doesn't move.
Category pages live separately at /[locale]/blog/category/<category> and are indexed by Google. Tag pages exist at /[locale]/blog/tag/<tag> but are noindex so thin tag pages don't pollute SEO.
translationKey wires hreflang automatically
Each post carries a translationKey in its frontmatter. Posts sharing the same key are treated as translations of one another. From that I generate <link rel="alternate" hrefLang> tags automatically.
Next.js 16 gotchas
Next.js 16 ships a few breaking changes that bite if you skim the docs.
middleware.ts is now proxy.ts
The middleware file convention has been renamed. next-intl's createMiddleware still works — it just goes in src/proxy.ts now.
// src/proxy.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
};
params is a Promise
Pages and layouts now receive params as a Promise. Synchronous access is a type error.
export default async function Page({ params }: PageProps<'/[locale]/blog/[slug]'>) {
const { locale, slug } = await params;
// ...
}
PageProps<'/...'> and LayoutProps<'/...'> are globally available — no need to hand-roll the param interface.
Turbopack + JSON imports
Velite's generated .velite/index.js re-exports JSON via with { type: 'json' }. Dev-mode Turbopack mangled certain JSON files for me through this path. The workaround was a tiny server-only helper using fs.readFileSync + JSON.parse. Production builds invoke it once per static page through generateStaticParams, so the cost is invisible.
Authoring workflow
Two scripts keep the friction near zero.
pnpm new:post "Title" --slug url-friendly-slug --locale en --category tech
# → content/blog/en/2026-05-24-url-friendly-slug.mdx as draft:true
# → public/images/blog/url-friendly-slug/ ready for assets
pnpm publish-post url-friendly-slug --locale en
# → flips draft:true to draft:false
Drafts are safe to commit — draft: true posts are excluded from listings, sitemap, and even direct URLs. I can scribble freely.
Images live in public/images/blog/<slug>/ and are referenced with plain Markdown image syntax. VS Code's Paste Image extension lets me drop screenshots straight from the clipboard.
What's next
Four content tracks going forward:
- tech — stack choices, implementation writeups, refactors
- product — building, growth, failure stories
- business — startup notes, market analysis
- essay — career, philosophy, longer-form thinking
The next near-term project is a React Three Fiber hero scene on the home page. The hero text stays as real HTML so SEO stays independent of the canvas — 3D layered on top as decoration.
The plumbing is done. Time to write.