Markdown source and a dependency-free static site generator for
blog.libcna.com. Node.js 20 or newer is the only build requirement.
The presentation follows the current CNA Light 1.0.2 ClassicPress theme: the same light palette, typography, content width, article cards, sidebar, navigation, and responsive behavior. The generated site also uses the same CNA favicon as the current ClassicPress blog.
- Node.js 20+ with native ECMAScript modules
- A dependency-free static site generator in
scripts/build.mjs - Markdown files with a small YAML-compatible metadata block
- Semantic HTML5 and a responsive CSS theme without a client-side framework
- Generated category, tag, year, month, and day archives
- Generated RSS, sitemap, canonical URLs, redirects, and a 404 page
- A small Node.js preview server and an internal-link validator
- A dependency-free importer for ClassicPress/MariaDB SQL dumps
- GitHub Actions for clean-build validation on pushes and pull requests
npm run build
npm run serveThe generated website is written to dist/ and committed together with its
source so the exact release artifact can be reviewed. The local preview is
available at http://localhost:8080. Run the complete validation with:
npm testCreate a directory matching the publication date, then copy the article template into it:
mkdir -p content/posts/2026/10/05
cp content/_templates/post.md.example content/posts/2026/10/05/article-slug.mdEvery article path follows this format:
content/posts/YYYY/MM/DD/article-slug.md
For example:
content/posts/2012/07/16/first-game-with-cna.md
generates:
/2012/07/16/first-game-with-cna/
Copy content/_templates/post.md.example
when starting an article. The title, date, and description fields are
required. The date must agree with the source directory. categories and
tags are lists; their archive pages are generated automatically.
---
title: Article title
date: 2026-10-05
updated: 2026-10-05
description: Short article description.
categories:
- Development
tags:
- CNA
- XNA
draft: false
---After creating the article, build and validate the entire site:
npm testStore images and downloads under public/media/. Reference them from the site
root. For example, an article can use this layout:
public/media/2026/what-is-cna/
architecture.webp
compatibility-report.pdf
Embed an image with standard Markdown:
Link a downloadable attachment in the same way:
[Download the compatibility report](/media/2026/what-is-cna/compatibility-report.pdf)Use descriptive lowercase filenames without spaces. Images and attachments are
copied to dist/media/ without modification and are intentionally tracked by
Git. Optimize large images before committing them; use external object storage
or Git LFS for unusually large downloads.
The .gitignore excludes dependencies, caches, logs, local environment files,
and workspace metadata. It deliberately does not ignore dist/ or
public/media/, so both the generated website and article assets are included
in commits. The CNA favicon is stored in public/favicon.svg and contains the
same 32×32 logo used by the current blog.libcna.com site.
Migrated ClassicPress media keeps its historical path below
public/wp-content/uploads/YYYY/MM/. This makes existing media URLs continue
to work. The migration retains both original files and ClassicPress-generated
size variants until all historical references have been audited.
Edit its existing Markdown file and run npm test. Keep the original date
and dated directory unchanged so the public URL remains stable. Set the
optional updated field after a substantial revision. Article cards, archive
lists, and full article pages display the last-update date; when updated is
omitted, they use the original publication date.
date: 2026-10-05
updated: 2026-10-12If a slug or permalink must change, place the old path in aliases so the
generator creates a redirect:
permalink: /2026/10/04/new-article-address/
aliases:
- /2026/10/04/old-article-address/Do not edit anything under dist/; it is generated from content/ and
public/ and is replaced by every build. After changing content, run
npm test and commit the updated source and generated dist/ files together.
The date field is also the publication date. A normal build includes only
articles dated today or earlier in the Europe/Prague timezone. A future-dated
Markdown file can therefore be committed without appearing in HTML, RSS, the
sitemap, or any archive until its date arrives. Its directory must still match
that date:
content/posts/2026/11/15/planned-article.md
date: 2026-11-15Preview scheduled articles locally with:
BUILD_FUTURE=1 npm run buildBuild and validate all scheduled articles in one command with:
npm run test:futureBUILD_DATE=YYYY-MM-DD npm run build overrides today's date for reproducible
testing. The hosting service must run a new build on or after the publication
date; date filtering does not itself trigger a deployment.
The repository includes a dependency-free importer that reads a standard MariaDB/MySQL SQL dump directly, including gzip-compressed dumps. Keep the raw database and ClassicPress installation outside this repository because they can contain passwords, e-mail addresses, salts, and plugin secrets.
Audit a backup without changing the repository:
node scripts/import-classicpress.mjs \
--database /safe/path/database.sql.gz \
--uploads /safe/path/classicpress/wp-content/uploads \
--reportPerform the conversion with --write. This deliberately replaces
content/posts/, so use it only with the authoritative backup:
node scripts/import-classicpress.mjs \
--database /safe/path/database.sql.gz \
--uploads /safe/path/classicpress/wp-content/uploads \
--writeThe importer replaces content/posts/, copies the supplied upload tree to
public/wp-content/uploads/, selects publish and future posts, converts
ClassicPress HTML to Markdown, restores categories and tags, resolves featured
images, preserves the numeric post ID and original URL, and validates every
local media reference. date comes from post_date; updated comes from the
actual post_modified value. Scheduled posts remain in source but are omitted
from a normal build until their publication date.
The completed 2026 migration produced 29 Markdown articles: 8 with ClassicPress
status publish and 21 with status future. See
docs/MIGRATION.md for implementation details.
- The default article permalink is
/year/month/day/slug/. - Set
permalinkwhen the original article used a different primary path. - Add old paths to
aliases; the generator creates redirect pages there. An alias must be a path, not a full URL or a URL containing a query string. - Record the original public URL in
originalUrl. - Record the original ClassicPress post ID in
classicpressIdwhen useful. - Every article appears in year, month, day, category, and tag archives.
See docs/MIGRATION.md for the complete migration process
and content format.
content/
posts/ articles arranged by year/month/day
pages/ standalone pages
_templates/ authoring templates, never generated
public/ static files copied without modification
scripts/build.mjs static site generator
scripts/check.mjs generated-site validation
scripts/import-classicpress.mjs SQL-to-Markdown migration tool
scripts/serve.mjs local preview server
dist/ generated website, committed as the release artifact
dist/ is a complete static website suitable for GitHub Pages, Cloudflare
Pages, Netlify, nginx, or ordinary static hosting. It contains a CNAME file
with blog.libcna.com; the source copy is maintained at public/CNAME and is
copied into every build.
.github/workflows/check.yml verifies every push and pull request.
.github/workflows/pages.yml builds the site, validates it, uploads only the
generated dist/ directory, and deploys that artifact through GitHub Pages.
The Pages workflow runs automatically after every push to main. It also runs
shortly after midnight for both possible Europe/Prague UTC offsets, allowing
scheduled articles to appear on their publication date across daylight-saving
time changes. It can still be started manually from the Actions tab.
GitHub Pages must use GitHub Actions as its publishing source, and the
repository Pages settings must define blog.libcna.com as the custom domain.
DNS should contain a CNAME record from blog.libcna.com to
libcna.github.io.
Every deployment runs npm test, so the uploaded artifact is a freshly built
and validated copy of dist/.
The project source is available under the MIT License; see LICENSE. Migrated articles must retain their actual authorship and licensing terms. The repository license does not automatically replace the license of existing content.