Zhanyin's tech stack is thin: Markdown for writing, a Node.js script for building, template literals for HTML, Cloudflare Pages for hosting. No framework, no bundler, no CMS.
The reason for this stack isn't "minimalism" — technical choices don't need to be moralized. The real reason is that I can predict the behavior of every link in this chain, and when something breaks I can trace it directly to the file responsible.
From Markdown to HTML
Each post is a .md file. Frontmatter holds metadata, body is Markdown.
The core build logic is under 300 lines. fast-glob finds all posts and pages, gray-matter splits frontmatter from body, markdown-it renders HTML. Heading anchors via markdown-it-anchor, footnotes via markdown-it-footnote, tables wrapped in a table-scroll container for mobile horizontal scrolling — all done at build time. The browser receives final HTML.
content/posts/*.md → loadPost() → { title, date, slug, html, tags, ... }
content/pages/*.md → loadPage() → { title, slug, html }
Post URLs are generated from filenames: the date prefix in 2026-05-21-slug.md is only for local sorting; the published path is /posts/slug/. Posts with draft: true are skipped unless INCLUDE_DRAFTS=true is set explicitly — half-finished drafts can stay in the repo without fear of accidental publishing.
The homepage is also entirely generated by the build script: featured posts fill the selected grid, the rest go into a year-grouped archive. No hand-written HTML, no CMS drag-and-drop.
Why Template Literals
The template system doesn't use JSX, Pug, or Handlebars. It's five plain functions returning template-literal HTML strings:
layout()— HTML shell:<head>, nav, footerhomePage()— homepage: intro section + featured grid + yearly archivepostPage()— post page: title, date, tags, bodysimplePage()— generic page, for About etc.escapeHtml()/attrs()— HTML escaping helpers
Function composition is straightforward: layout({ body: postPage({ post }) }). No template inheritance, no partials, no slots — each function independently produces a complete chunk of HTML, with only one layer of calls.
The tradeoff is obvious: changing the nav structure means manually checking everywhere it's used. The benefit is zero "template engine magic" — open the file and the complete data flow is right there. A single escapeHtml() call traces to its definition. For a personal site where templates change a few times a year, this predictability matters more than template engine conveniences.
Why Plain CSS
Styles live in a single styles.css file, about 700 lines. CSS custom properties for theme variables, @media for responsive design, no build step.
Not using Tailwind isn't a stance against Tailwind. Tailwind has clear advantages in team collaboration and componentized projects — constraining class naming, narrowing style scope, reducing the risk of ad-hoc CSS. But those advantages don't apply here: one maintainer, under five components, no style conflict issues. Adding Tailwind would only introduce a build step and a config file to track.
Conversely, plain CSS preserves direct modification ability. Adjusting a color doesn't require going through utility class layers, recompiling, or understanding framework-generated CSS — open styles.css, search, change the value, done.
Font stack, typography scale, color variables, scroll containers, table handling — all in one file, organized by visual hierarchy. Maintenance cost is one file's worth of grep.
Deployment
Cloudflare Pages listens for repo pushes, runs npm run build, outputs dist/. The build output is fully static — HTML, CSS, RSS, sitemap, robots, favicon.
There's no server-side runtime. Cloudflare Pages doesn't need to understand Markdown, frontmatter, or templates — it just hosts static files. If I want to switch hosting platforms later, moving dist/ to any static hosting service works. Migration cost equals one npm run build.
RSS is generated using the feed library, pulling the latest 20 posts, auto-updated each build. Sitemap and robots are also generated at build time, XML strings concatenated by hand — the content is just a few lines, not worth adding a dependency for.
The Boundaries of This Stack
This isn't a "general-purpose lightweight blog solution" — it works at this site's scale.
No admin panel — writing a post means editing a Markdown file and git pushing. No site search, no comment system, no automatic image compression, no i18n, no theme switching. Every new feature requires hand-written code; there's no "install a plugin" shortcut.
These limitations are acceptable for now. The blog's first goal is letting me keep writing; the second is letting readers read smoothly. As long as Markdown rendering doesn't break, pages load fast, and RSS updates correctly, everything else is unnecessary complexity at this stage.
Closing
If there's one thing about this setup I especially value, it's not "lightweight" or "simple" — it's predictability.
Build script: 300 lines. Templates: 200 lines. Styles: one file. When I change something, the impact doesn't need guessing. When a bug appears, I don't need to investigate framework internals or suspect plugin side effects. For a personal blog, the thing that drains your willpower isn't writing code — it's hitting a problem and not knowing where to start debugging.
Predictability doesn't come up much in technical discussions, probably because it's not sexy enough. But for something you plan to maintain long-term, it might be the most important metric.