Blogtinum began with a simple preference: the durable part of a publication should be the writing, not the application currently displaying it. My posts should remain ordinary Markdown files that I can read, diff, move, and rebuild without asking a hosted platform for an export.
This is not a story about rewriting every dependency. Blogtinum owns the content contract,
orchestration, validation, and output pipeline, while deliberately relying on focused tools such as
markdown-it, Shiki, KaTeX, Vue’s server renderer, and esbuild. The goal is control over the system,
not the vanity of implementing a Markdown parser from scratch.
Why build another publishing engine?
The previous site used Hexo, and Hexo did its job well: take source content, combine it with a theme and plugins, and emit a static website. The reason to move was not that static site generators had failed. It was that the publication had grown into a more specific product:
- one repository needed to build several sites with explicit configuration;
- public pages and the Admin preview needed exactly the same Markdown behavior;
- bilingual variants needed a first-class identity instead of a naming convention hidden in a theme;
- publishing needed both local-file and GitHub-backed workflows;
- metadata, redirects, feeds, search, and social previews needed one typed model;
- interactive features needed to stay small instead of turning every page into a client-rendered app.
Owning the pipeline also makes the archive less dependent on a theme ecosystem. Content lives under
content/<site>/posts/<slug>/index.<lang>.md, media sits beside it, and Git records every meaningful
change. Rendering is replaceable; the source remains useful.
A publishing system is temporary. Plain text with a clear schema is the asset.
What is a static site generator?
A static content generator moves work from request time to build time. Instead of asking a server to query a database and assemble the same article for every visitor, it does that work once and writes the result to disk.
At request time, the host mostly maps a URL to a file. If pages are built once and each page is requested times, the expensive rendering work is closer to per deployment than across readers. That simplification brings several practical benefits:
| Property | Static build | Request-time application |
|---|---|---|
| Typical article request | Read a prepared file | Execute application logic |
| Scaling | CDN handles repeated reads | Runtime must absorb traffic |
| Attack surface | No content database on the public path | Server and data store remain exposed |
| Content update | Rebuild and deploy | Usually visible after a write |
| Per-reader personalization | Limited without an API | Natural fit |
Hexo, Hugo, Eleventy, and many documentation tools follow this broad model. They differ in language, theme system, plugin model, and content conventions, but “static” describes the output, not an absence of capable tooling.
Why replace Hexo here?
Hexo remains a sensible choice for a conventional blog. It has mature themes, a familiar plugin ecosystem, and a quick path from Markdown to deployment. Blogtinum trades that convenience for a smaller, repository-specific contract.
The deciding factor was one rendering path. A plugin configuration in the site generator and a
second approximation in an editor preview will eventually disagree. A code block, relative image,
heading ID, or custom container may look correct while editing and change after publishing.
Blogtinum instead exposes one renderer from @blogtinum/core; the CLI build and Admin preview both
call it.
One renderer, two consumers
The public build and live preview may wrap the rendered HTML differently, but they do not maintain two Markdown interpretations. Fixing a renderer bug fixes both surfaces.
The migration is therefore not “custom is always better.” It is a boundary decision: use a general generator while its conventions fit, and own the orchestration when the publication’s contract becomes the feature.
How the Blogtinum engine works
The repository is split by responsibility, and dependency direction stays simple:
@blogtinum/shared-typesdefines posts, site settings, authors, validation results, and storage contracts.@blogtinum/coreloads content, renders Markdown, derives excerpts and reading time, validates references, groups translations, and produces SEO/search/feed data.@blogtinum/cliselects a site, applies the publish gate, asks the public Vue app to SSR every route, and writes the complete static output.apps/siteturns typed page data into HTML. Its client entry hydrates only the interactive parts that need browser state.apps/admin-apiandapps/blog-adminprovide authoring without becoming a dependency of the public site.
1. Load one content model
YAML frontmatter describes stable metadata such as title, slug, language, categories, tags, cover,
publication flags, and aliases. The filename identifies the language variant, while
translationKey groups variants into one logical article.
The loader parses that source, normalizes optional booleans, resolves the route, renders the body, and derives values that authors should not maintain by hand:
const { html, toc } = renderer.render(markdown, { assetBase });
const text = plainText(html);
const words = wordCount(text);
return {
html,
toc,
wordCount: words,
readingTime: readingTime(words),
};
2. Render Markdown once
@blogtinum/core configures markdown-it once, then installs the syntax features used by both
production and preview. Shiki highlights fenced code; KaTeX renders math; Mermaid source becomes a
progressively hydrated diagram; plugins add anchors, footnotes, admonitions, tabs, and media path
resolution.
For a rough reading-time estimate, the engine uses a simple relationship such as
where is the derived word count. It is intentionally an estimate, not a promise about a reader.
3. Prerender complete routes
The CLI does more than transform Markdown into an HTML fragment. It prepares typed PageData for
the home page, posts, archives, categories, tags, series, and standalone pages. Vue SSR renders those
views to complete page markup, and the generator writes each route to its final directory.
The same build emits:
- theme CSS, fonts, media, and a small browser bundle;
sitemap.xml, RSS, canonical and alternate-language metadata;- OpenGraph, Twitter metadata, and JSON-LD;
- a static search index consumed by Fuse.js;
- generated social images when an author cover is absent;
_redirectsentries for legacy aliases.
4. Hydrate only where interaction helps
The generated article is useful before JavaScript runs. Search, table-of-contents scroll-spy, Mermaid, copy buttons, tabs, overlays, and theme switching are enhanced in the browser. They are small islands around static content rather than a requirement for the first readable render.
Tip
“Static-first” does not mean “no JavaScript.” It means HTML carries the primary experience, and JavaScript earns its place by adding an interaction.
Supported writing syntax
This article itself is a renderer check. The headings above populate the table of contents, the diagram is authored as Mermaid, the formula is KaTeX, and the code block includes a filename and highlighted lines.
Standard Markdown covers paragraphs, strong emphasis, emphasis, lists, links, block quotes,
inline code, images, and tables. Relative media is resolved from the post directory, so the source
stays portable:
Fenced blocks support syntax highlighting, copy controls, optional filenames, and line ranges. Math works inline, as in , or in display blocks. Footnotes keep longer qualifications out of the main sentence.[1]
Admonitions can express intent without inventing raw HTML for every callout:
Verify sample content
Posts created only to populate a development site carry an AI Generated provenance label. It is not a taxonomy tag and does not create an archive route. Readers should verify those sample articles before relying on them.
Platform-specific instructions can share one tabbed block:
The local adapter writes Markdown and co-located media directly into content/ for development.
The hosted adapter writes through the GitHub API so publishing remains a reviewable Git change.
:::
Longer supporting material can use a native details disclosure:
What remains readable without client enhancement?
The article body, headings, links, code, math output, tables, and tab panels are present in the server-rendered document. Browser code adds convenience and stateful behavior.
The Admin as a Git-backed headless CMS
Blogtinum’s Admin is “headless” in a practical sense: it edits the same content model, but it does not serve the public article on every request. The public site only depends on generated artifacts; the authoring interface can be offline, redeployed, or replaced without changing reader URLs.
The Vue Admin talks to an Express BFF through typed JSON contracts. Two storage adapters implement the same boundary:
LocalFsAdapterreads and writes the repository directly during local development.GitHubApiAdapterreads files and creates commits through GitHub for hosted authoring.
The editor manages frontmatter, Markdown, media, language variants, local draft recovery, revision history, and publishing state. Preview calls the shared core renderer instead of reproducing Markdown in the browser. A publish operation validates content before it reaches the deployment branch, after which the normal static build produces the public site.
This separation keeps responsibilities honest: Admin is a content client, Git is the durable store, the generator is the delivery pipeline, and a CDN serves readers.
Where static-first is the wrong fit
Static generation is an excellent default for articles, documentation, portfolios, and other read-heavy content. It is not automatically correct for every product. A dashboard with private per-user data, an inventory changing every second, or a workflow that must confirm a transaction at request time needs a runtime service.
Blogtinum also accepts tradeoffs:
- changing published content requires a build and deployment;
- a very large archive may need incremental generation later;
- comments, forms, analytics, and private data require external services or APIs;
- a custom contract means maintaining it instead of inheriting every feature from an ecosystem.
The rule is not “make everything static.” It is “keep the stable reading path static, then add the smallest runtime boundary for genuinely dynamic behavior.”
Hello, for real
Blogtinum exists so the source remains boring and the delivery path remains explicit. Markdown is the archive. The core is the interpretation. The CLI turns that interpretation into durable files. The public UI presents them, and Admin makes the same model comfortable to edit.
That is a smaller promise than replacing every publishing platform, but it is one I can inspect, test, and keep.
“Owning the content” means retaining the original text, media, history, and buildable schema. It does not mean avoiding open-source libraries; those are deliberately replaceable implementation details behind the repository’s contracts. ↩︎