Building This Site

March 21, 2026

The idea of building a personal website had been on my mind for years. I bought the domain back in 2022 but never actually shipped it. This time was different — one evening, one terminal window, a few words with AI, and the site was live.

This post documents the full process from tech decisions to deployment.

What I wanted

The requirements were simple:

  • Write Markdown, push to git, auto-publish
  • No CMS, no database
  • Minimal black-and-white design
  • Bilingual support (Chinese and English)
  • Near-zero maintenance cost

Tech stack

DecisionChoiceWhy
FrameworkNext.js (App Router)MDX support, static generation, extensible
HostingVercelPush to deploy, free SSL
Content.md/.mdx in content/No database, git-driven
MDX renderingnext-mdx-remote + gray-matterFrontmatter parsing
StylingTailwind CSS v4A minimal site doesn't need a component library
Code highlightingsugar-high1KB, zero config
Package managerpnpmFast

I didn't go with Hexo, Hugo, or other static site generators because Next.js gives more flexibility — adding comments, subscriptions, or OG image generation later is straightforward.

Project structure

bryantchen.cc/
├── app/
   ├── layout.tsx              # Root layout
   ├── [lang]/                 # i18n routing
      ├── layout.tsx          # Lang layout (nav, footer)
      ├── page.tsx            # Home
      ├── thoughts/page.tsx   # Thoughts list
      ├── posts/
         ├── page.tsx        # Posts list
         └── [slug]/page.tsx # Single post
      ├── projects/page.tsx   # Projects
      └── about/page.tsx      # About
├── content/
   ├── zh/                     # Chinese content
      ├── thoughts/
      └── posts/
   └── en/                     # English content
       ├── thoughts/
       └── posts/
├── lib/
   ├── content.ts              # Content reader
   ├── i18n.ts                 # i18n dictionaries
   └── projects.ts             # Project data
├── components/                 # UI components
├── middleware.ts               # Locale redirect
└── next.config.ts

Content model

Thoughts

Short-form, no title. The filename is the slug:

// content/en/thoughts/2026-03-21-hello.md
---
date: "2026-03-21"
tags: ["life"]
---
A few sentences about whatever's on my mind.

Posts

Long-form with title and description:

// content/en/posts/some-post.mdx
---
title: "Post Title"
date: "2026-03-21"
description: "One-line summary"
tags: ["tag"]
draft: false
---
Full post content with Markdown and MDX support...

lib/content.ts reads these files at build time, parses frontmatter with gray-matter, and sorts by date descending. Posts with draft: true are excluded from production.

Internationalization

This is the most interesting architectural piece.

The core idea is using Next.js App Router's [lang] dynamic segment:

  • /zh/posts/xxx shows Chinese
  • /en/posts/xxx shows English
  • / auto-redirects to /zh
// middleware.ts — auto-redirect to default locale
export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const hasLocale = locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );
  if (hasLocale) return;
  const url = request.nextUrl.clone();
  url.pathname = `/${defaultLocale}${pathname}`;
  return NextResponse.redirect(url);
}

UI strings (nav labels, headings, empty states) are managed with a plain dictionary object — no third-party i18n library:

// lib/i18n.ts
const dictionaries = {
  zh: {
    nav: { thoughts: "碎碎念", posts: "文章", ... },
    ...
  },
  en: {
    nav: { thoughts: "thoughts", posts: "posts", ... },
    ...
  },
};

Content is split by locale directory. Each language is independent — if a post exists in Chinese but not English, the English site simply doesn't show it. No errors.

A language toggle link in the top-right corner of the nav switches between Chinese and English.

Deployment

Three steps, under 10 minutes total:

1. Push to GitHub

git init && git add -A && git commit -m "Initial commit"
gh repo create bryant24hao/bryantchen.cc --public --source . --push

2. Import to Vercel

Open Vercel, pick the GitHub repo, click Deploy. Next.js is auto-detected — zero configuration needed.

3. Domain setup

Add bryantchen.cc in Vercel Settings → Domains, then add two DNS records in Cloudflare:

TypeNameValue
A@IP from Vercel
CNAMEwwwVercel's project-specific address

Important: set Proxy status to DNS only (gray cloud) so Vercel handles SSL.

SSL is auto-provisioned in a few minutes. The site is live on the custom domain.

Security hardening

A static site has a small attack surface, but a few defensive measures are still worth doing:

  • Locale parameter validation — invalid paths return 404, preventing path traversal
  • Security response headersX-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy
  • .env leak prevention — gitignore covers bare .env files
  • Privacy checks — scan for real IPs, passwords, and API keys before every commit

Writing workflow

Publishing content is now:

# 1. Create a file
vim content/en/thoughts/2026-03-22-some-thought.md

# 2. Write content (frontmatter + body)

# 3. Publish
git add content/ && git commit -m "New thought" && git push

Vercel picks up the push and deploys in ~30 seconds. No admin dashboard needed.

What's next

The MVP is live. Things I want to add later:

  • RSS feed
  • Auto-generated OG images
  • Giscus comments (GitHub Discussions-based)
  • Email subscriptions
  • Full-text search
  • Tag filtering

But no rush. Content first, features later.

Final thought

From idea to live site, it took one evening. I used to think building a personal site was a big project. Looking back, the real barrier was never the tech — it was just starting. The tools are good enough now — describe what you want, AI builds the skeleton, you fill in the content.

If you've been meaning to build your own site, now is the best time.