Building a custom blog from scratch in 2026 is a deliberate choice. Most writers just pick a hosted platform and move on. I decided to rebuild my tech blog with Astro because I wanted to own every layer of the stack and to learn patterns that actually make a static site faster, not just different. The result is a bilingual site that serves Japanese and English content, ships zero JavaScript by default, and keeps every ounce of complexity on my machine rather than the visitor’s browser.

Here are the five patterns that made it work.

Content Collections with Zod

Astro’s Content Collections do more than organize Markdown files into folders. They enforce a contract between your content and your code. I attached a Zod schema to every article collection, which means the build step validates frontmatter before a single page renders.

The schema requires a language field that accepts only two values: ja or en. There is no ambiguity about which language a reader will get. I also added a pair field that links translations together. If I publish a post about Astro in Japanese and later translate it into English, both files share the same pair ID. This makes building a language switcher trivial because the relationship is explicit in the data, not inferred from file names.

Dates arrive from Markdown frontmatter as strings, so the schema coerces them into real Date objects automatically. That eliminates string-mangling logic inside my page templates. The most important benefit, though, is the failure mode. If a file is missing a required field or uses an invalid language code, the build crashes immediately with a clear error. I fix it in my terminal instead of discovering a broken layout or a silent 404 after deployment.

Article Conversion Pipeline

I did not write every post in this new format from day one. Years of content lived on Zenn and Dev.to, each platform with its own quirks and proprietary syntax. Rather than copy-pasting and fixing by hand, I wrote a TypeScript script that converts entire batches of articles into standard Markdown.

Zenn uses custom callout syntax for tips and warnings. My script turns those into semantic HTML aside tags so they render consistently across the site. Dev.to relies on Liquid tags for embeds and special blocks. The pipeline translates those into plain Markdown links that work anywhere.

Some posts contain asides meant only for the original platform, like a disclaimer about Medium paywalls or a Zenn-specific image path. I wrap those in HTML comments so the converter can strip them out during migration. The script processes files line by line, but it respects code boundaries. When it detects a fenced code block, it skips the transformation rules entirely. Corrupting a syntax sample would undermine the whole purpose of a tech blog, so the line-by-line parser treats code blocks as untouchable zones.

Running one command now republishes years of writing without breaking a single link or callout.

Build-Time OGP Image Generation

Social sharing images are usually an afterthought. You either design them manually or install a heavy runtime service that generates cards on demand. I wanted neither. Every Open Graph image on this site is produced during the build so that visitors receive nothing more than a lightweight img tag pointing to a static PNG.

I use Satori, which takes JSX markup and renders it to SVG. The output is crisp, predictable, and easy to template. The real optimization came from font handling. A full Japanese web font can easily top five megabytes. Loading that during the build, let alone asking a browser to fetch it, would be absurd.

Instead, I use Google Fonts subsetting. The script inspects the title text for a given post and requests only the exact glyph set needed to render that string. If a headline uses forty unique Japanese characters, only those forty characters travel across the wire. The build stays fast, and the rendered image never shows broken tofu blocks because the subset is precise. Nothing is left to runtime chance.

Dark Mode via Tailwind Tokens

I refused to decorate every element with dark: utility classes. That approach scales poorly and litters your markup with noise. I redefined the color tokens themselves so the same class name resolves to different values depending on the active theme.

저는 모든 표면(surface)과 텍스트 색상에 CSS custom properties를 사용합니다. 라이트 모드에서 --color-white#ffffff에 매핑됩니다. 다크 모드에서는 동일한 변수 이름이 검정에 가까운 값을 가리킵니다. 제 HTML은 이 환경에 완전히 무관하게 유지됩니다. 카드는 시간대에 상관없이 bg-ui-surfacetext-ui-primary를 사용할 수 있습니다. 테마 전환은 루트(root)에서 변수 정의를 변경하며, 전체 인터페이스가 즉각적으로 반응합니다.

이 방식의 유일한 위험은 스타일시트가 로드되기 전에 라이트 모드 콘텐츠가 순간적으로 보이는 현상입니다. 저는 이를 document head에 아주 작은 인라인 스크립트를 넣어 해결했습니다. 이 스크립트는 첫 번째 페인트(first paint)가 일어나기 전에 실행되어, localStorage와 시스템 설정을 확인하고 즉시 올바른 data 속성을 설정합니다. 스크립트가 렌더링을 차단하는 시간은 불과 몇 밀리초에 불과하므로, 방문자는 다크 모드가 적용되기 전 눈부신 흰색 깜빡임을 경험하지 않습니다.

아일랜드 아키텍처와 Zero-JS

Astro의 핵심 전제는 페이지가 정적 HTML로 시작해야 한다는 것입니다. 자바스크립트는 상호작용이 진정으로 필요할 때만 개입합니다. 저는 이 원칙을 엄격히 따랐습니다.

글로벌 메뉴와 테마 토글에는 React를 사용하지 않았습니다. 두 기능 모두 단일 모듈에 포함된 소량의 바닐라 자바스크립트로 처리됩니다. 하이드레이션 오버헤드도, 가상 DOM 디핑(diffing)도, 다운로드해야 할 프레임워크 런타임도 없습니다.

제가 사용하는 유일한 무거운 라이브러리는 텍스트로 다이어그램을 렌더링하는 Mermaid.js입니다. 이를 전역으로 임포트하는 대신, Intersection Observer로 감싸서 처리했습니다. 옵저버는 다이어그램 컨테이너를 감시합니다. 사용자가 다이어그램 근처 수백 픽셀 이내로 스크롤하면, 스크립트가 동적으로 Mermaid 모듈을 주입하고 다이어그램을 렌더링합니다. 포스트에 다이어그램이 없다면 해당 라이브러리는 네트워크를 전혀 사용하지 않습니다. 초기 페이지 로드는 가볍게 유지되며, 브라우저는 독자가 실제로 보는 부분에 대해서만 비용을 지불합니다.

빌드 우선 사고방식 (The Build-First Mindset)

모든 패턴을 관통하는 흐름은 간단합니다. 빌드 과정에서 할 수 있는 작업이라면, 거기서 처리하십시오. 사이트가 배포되기 전에 Zod로 데이터를 검증하십시오. 요청 시점이 아닌 사전에 독자적인 플랫폼 구문을 변환하십시오. 서버를 구동하는 대신 소셜 이미지를 정적 파일로 렌더링하십시오. 모든 클라이언트에 로직을 전달하는 대신 토큰을 통해 테마 색상을 해결하십시오. 무거운 자바스크립트는 사용자가 실제로 필요로 할 때까지 지연시키십시오.

복잡성을 빌드 단계로 앞당김(pushing complexity leftward)으로써 런타임을 예측 가능하게 유지하고, 페이로드를 작게 만들며, 유지보수 부담을 관리 가능한 수준으로 유지할 수 있습니다. 사이트가 빠른 이유는 어떤 단일한 트릭 때문이 아니라, 방문자의 브라우저 내부에서 일어나는 일이 단순히 적기 때문입니다. 이것이 2026년에 정적 아키텍처를 선택했을 때 얻는 진정한 보상입니다.