rammaru.com
·Tech

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

AreaChoice
FrameworkNext.js 16 (App Router, Turbopack)
i18nnext-intl 4
ContentMDX + Velite (Zod schemas)
StylingTailwind CSS 4
HostingVercel
3D slotReact 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.