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.

אני משתמש במאפיינים מותאמים אישית של CSS עבור כל משטח וצבע טקסט. במצב בהיר (light mode), --color-white מקושר ל-#ffffff. במצב כהה (dark mode), אותו שם משתנה מצביע על ערך קרוב לשחור. ה-HTML שלי נשאר אגנוסטי לחלוטין. כרטיס יכול להשתמש ב-bg-ui-surface וב-text-ui-primary מבלי לדאוג לשעה ביום. החלפת הנושא (theme switch) משנה את הגדרות המשתנים בשורש (root), וכל הממשק מגיב באופן מיידי.

הסיכון היחיד בגישה זו הוא הבהוב (flash) של תוכן בהיר לפני טעינת ה-stylesheets. פתרתי זאת באמצעות סקריפט inline קטן ב-head של המסמך. הוא רץ לפני הציור הראשון (first paint), בודק את ה-localStorage ואת העדפת המערכת, וקובע את תכונת הנתונים (data attribute) הנכונה באופן מיידי. מכיוון שהסקריפט חוסם את הרינדור למשך כמה מילישניות בלבד, המבקר לעולם לא רואה התפרצות לבנה ומציקה לפני שהמצב הכהה נכנס לפעולה.

ארכיטקטורת איים (Island Architecture) ו-Zero-JS

הנחת היסוד של Astro היא שדף צריך להתחיל כ-HTML סטטי. JavaScript נכנס לתמונה רק כאשר אינטראקציה דורשת זאת באמת. לקחתי את זה ברצינות.

נמנעתי משימוש ב-React עבור התפריט הגלובלי ומעבר הנושא (theme toggle). שניהם מטופלים באמצעות כמות קטנה של vanilla JavaScript שמתקיימת במודול בודד. אין עומס של hydration, אין השוואת virtual DOM (diffing), ואין framework runtime להורדה.

הספרייה הכבדה היחידה שאני משתמש בה היא Mermaid.js עבור רינדור דיאגרמות מטקסט. במקום לייבא אותה גלובלית, עטפתי אותה בתוך Intersection Observer. ה-observer עוקב אחר מכולות (containers) של דיאגרמות. כאשר משתמש גולל למרחק של כמה מאות פיקסלים מאחת מהן, הסקריפט מזריק באופן דינמי את מודול Mermaid ומבצע רינדור לדיאגרמה. אם פוסט אינו מכיל דיאגרמות, הספרייה הזו לעולם לא נוגעת ברשת. טעינת הדף הראשונית נשארת קלה, והדפדפן "משלם" רק על מה שהקורא רואה בפועל.

גישת ה-Build-First

החוט המקשר בין כל התבניות הוא פשוט: אם אתה יכול לבצע את העבודה במהלך ה-build, עשה זאת שם. בצע וולידציה (Validate) לנתונים שלך באמצעות Zod לפני שהאתר נפרס (deploys). המר תחביר של פלטפורמות קנייניות מראש במקום בזמן הבקשה (request time). רנדר תמונות לרשתות חברתיות לקבצים סטטיים במקום להפעיל שרת. פתור צבעי נושא באמצעות tokens במקום לשלוח לוגיקה לכל לקוח (client). דחה (Defer) JavaScript כבד עד שהמשתמש באמת יזדקק לו.

דחיפת המורכבות שמאלה (leftward) אל שלב ה-build שומרת על ה-runtime צפוי, על ה-payload קטן ועל נטל התחזוקה בר-ניהול. האתר נשאר מהיר לא בזכות טריק בודד, אלא פשוט כי קורה פחות בתוך הדפדפן של המבקר. זהו התגמול האמיתי על בחירה בארכיטקטורה סטטית בשנת 2026.