Building This Blog Without Building a CMS
· 6 min readengineering #typescript #static-site #web-development
This blog began with a small idea: I wanted one place to write in English and Persian, show a few projects, and keep the whole thing understandable without operating a CMS.
That left out a lot of machinery. There is no database, server application, content API, admin panel, or client-side framework. The repository is the source of truth. Content lives in files, a Node and TypeScript build turns those files into static HTML, and the generated dist/ directory is what gets published.
This blog fits these requirement because one person edits the content, the structure is simple, and deployment should not be a burdon.
Project Structure
The project structure is something like this:
Blog/
├── posts/
│ ├── en/
│ │ └── building-this-blog/
│ │ └── index.md
│ └── fa/
│ └── building-this-blog/
│ └── index.md
├── pages/
│ ├── en/
│ │ └── about.html
│ └── fa/
│ └── about.html
├── src/
│ └── templates/
└── public/
Content stays in files
Posts and projects follow the same layout:
posts/en/<slug>/index.md
posts/fa/<slug>/index.md
projects/en/<slug>/index.md
projects/fa/<slug>/index.md
Each Markdown file starts with front matter section. The build uses it for the title, summary, date, publication status, categories, tags, and translation link.
---
title: Building This Blog Without Building a CMS
summary: A short description for list pages.
language: en
date: 2026-08-12
published: false
categories: [engineering]
tags: [typescript, static-site]
translationOf: building-this-blog
---
translationOf links English and Persian entries with the same value. When a reader switches language on a post or project page, the site can send them to that page's translation instead of other pages.
Standalone pages, such as About and Contact, are simpler still. They are editable HTML fragments in pages/en/ and pages/fa/. The build adds the document shell, navigation, and footer.
What the build produces
The build reads the content directories, checks required front matter, and creates pages for published entries. It generates language home pages, post and project detail pages, project indexes, category and tag indexes, standalone pages, a root redirect, robots.txt, a sitemap, and a 404 page.
The result is plain static HTML, so Node.js does not need to run on the server. Any host that serves static files can publish the site.
I keep the build narrow. It is not trying to become a general-purpose publishing platform. Search, comments, accounts, or an editor for multiple authors would be reasons to reconsider the architecture. Until then, they would mostly be code waiting for a problem.
Editable templates, shared layout
The shared page shell is plain HTML under src/templates/. It uses small placeholders such as {{title}}, {{content}}, and {{navigation}}.
TypeScript computes the repeated parts, escapes untrusted content, and inserts the result into the templates. A full template language would be useful if the templates needed loops, branching, or inheritance. This site does not need them.
The same approach handles both languages without duplicating the layout. English uses LTR markup. Persian uses RTL markup and the Vazir font. The language selector keeps the current route when an equivalent page exists, including project lists and static pages such as About and Contact.
Markdown and browser behavior
Posts use GitHub Flavored Markdown1 for tables, task lists, strikethrough, autolinks, and fenced code blocks.
Raw HTML inside Markdown is escaped rather than rendered. Markdown is for article content; shared HTML templates are for site structure. That keeps the content format predictable and stops arbitrary markup in a post from quietly changing the page.
The site uses a small amount of browser-side JavaScript where the browser is the right place for it:
- The theme toggle stores its choice in
localStorageand otherwise follows the system preference. - The language selector remembers the language a reader chose.
- The contact email link is assembled in the browser from separate attributes. It discourages basic address harvesting, but it is not a substitute for a real contact form if spam becomes a problem.
The category menu is a native <details> element, so it does not need JavaScript.
Checks before publishing
A content site can still break. Missing required fields, bad dates, language mismatches, or unresolved template placeholders can all leave broken pages behind.
The test suite covers front matter validation, bilingual links, pinned projects on the home page, static assets, templates, route-preserving language switches, and GFM rendering. Before publishing, I run:
npm test
npm run lint
npm run build
npm run build clears and recreates dist/, then copies public assets and files that belong beside a post or project. The generated output is disposable. The source files are what matter.
Publishing then has a short checklist:
- Run the validation commands locally.
- Build the site and confirm
dist/contains the expected pages. - Configure a static host to serve
dist/as the publish directory. - Point the domain at that host and set the canonical site URL used by the build.
- Let CI run the same checks for pushes or release tags.
The hosting provider is not the important part. Publishing only needs static files, which keeps the site portable and leaves room to change hosts without rewriting the blog.