Static blog generator that turns Markdown files into SEO-friendly, fully static HTML (no framework runtime). Output is written to dist/ and can be published directly (GitHub Pages supported out of the box).
- Builds HTML pages from Markdown in
content/posts/ - Generates an index (optionally paginated) and one folder-per-post under
dist/<slug>/index.html(public URL/<slug>/; pagination: site root/,/page/N/) - Copies static assets from
src/assets/intodist/assets/ - Adds basic SEO metadata (canonical URL, OpenGraph, Twitter cards)
- Auto-embeds YouTube links and preserves inline HTML when rendering Markdown
- Node.js: the workflow uses Node 20; any recent Node 18+ should work
- npm:
package-lock.jsonis present, sonpm ciis supported
- Install:
npm ci(ornpm install) - Build:
npm run build- Writes output to
dist/ - Respects
BASE_PATH,SITE_URL, andCNAME(see below)
- Writes output to
- Dev server:
npm run dev- Runs a local server (default
http://127.0.0.1:4173/) - Rebuilds when Markdown under the active posts directory or files under
src/change - Always uses
BASE_PATH=/for local convenience
- Runs a local server (default
- Clean:
npm run clean(removesdist/)
scripts/build.mjs: static site build (Markdown → HTML, templates, pagination, SEO metas)scripts/dev.mjs: rebuild-on-change + tiny HTTP server fordist/scripts/clean.mjs: deletesdist/scripts/lib/contentTransform.js: transforms content (YouTube URL → embed,<img>→ Markdown image)scripts/lib/postMeta.js: slug/title/excerpt + media detection for OG imagessrc/config.js: site title/tagline + pagination/excerpt settingssrc/templates/page.html: index template (list + pagination + “New Post” button)src/templates/post.html: post template (article + “Edit on GitHub” button)src/assets/*: CSS/JS/favicon copied todist/assets/dist/: generated output (publish this directory)
The builder supports “root sites” and “project pages” style hosting:
BASE_PATH: prefix used for links and asset URLs (always normalized to end with/)- Examples:
/or/my-repo/
- Examples:
SITE_URL: optional absolute site origin used to form canonical URLs and absolute OG image URLs- Example:
https://username.github.io
- Example:
When SITE_URL is not set, canonical/OG URLs remain relative (still valid for local previews).
Used by scripts/build.mjs:
BASE_PATH: URL prefix for links/assets (default:/)SITE_URL: absolute site origin for canonical URLs (default:src/config.jssite.url, or empty)CNAME: if set, writesdist/CNAMEwith this valuePOSTS_DIR: absolute or cwd-relative folder of*.mdposts (default:content/postsin this repo). CLI:npm run build -- --posts path/to/postsSITE_TITLE,SITE_TAGLINE,SITE_COMPANY_NAME: when set to a non-empty string, override the matching fields fromsrc/config.js(footer usesSITE_COMPANY_NAME). Used by the posts-only reusable workflow viawith:inputssite_title,site_tagline,site_company_name.GITHUB_EDIT_REPO:owner/nameof the repo where Markdown is edited (embedded asdata-gh-repofor “Edit on GitHub” / “New post”). If unset, the client tries to inferownerandrepofrom ahttps://*.github.io/<repo>/Pages URL (custom domains need this set at build time).GITHUB_POSTS_PATH: folder inside that repo containing*.mdfiles, no leading slash (default:content/postswhen building the defaultcontent/poststree; otherwise""for repo-root posts). Set explicitly when your layout does not match (e.g.content/postsin a posts-only repo).GITHUB_DEFAULT_BRANCH: branch used in edit/new URLs (defaultmain).GOOGLE_SITE_VERIFICATION: optional Google Search Console HTML tag token only (not the full meta tag). When set, every built page includes<meta name="google-site-verification" content="…" />. In Actions, set repo secretGOOGLE_SITE_VERIFICATION(engine repo) or passgoogle_site_verificationfrom the posts-only workflowwith:block /${{ secrets.GOOGLE_SITE_VERIFICATION }}.
There’s a ready-to-use workflow at .github/workflows/deploy.yml:
- Builds on pushes to
main - Uses:
BASE_PATH=/<repo>/SITE_URL=https://<owner>.github.io
- Uploads
dist/as the Pages artifact and deploys it
You can host only posts in a separate GitHub repo and still build with this generator—no copy of scripts/, src/, or package.json there.
- Create a new repository.
- Put every post as a
*.mdfile at the root of that repo (same naming style as here works well, e.g.001.md,052.md). - Add the workflow file: copy
examples/posts-only-repo/.github/workflows/deploy.ymlto.github/workflows/deploy.ymlin the new repo (it calls the reusable workflow in this repo). - In that repo: Settings → Pages → Build and deployment → Source: GitHub Actions.
- Push to
main. The site is published underhttps://<your-username>.github.io/<that-repo-name>/(project pages), usingStCost/blogas the engine.
The reusable workflow sets GITHUB_EDIT_REPO to your posts repo and GITHUB_POSTS_PATH from posts_subpath, so “Edit on GitHub” and “New post” open the correct path (repo root vs content/posts/, etc.).
Optional google_site_verification in the same with: block (or a repo secret passed through) emits the Google indexing verification meta tag on all pages.
If you prefer a subfolder for Markdown (e.g. content/posts/), set posts_subpath: content/posts in the workflow’s with: block in the posts-only repo.
Optional branding for that site only: in the same with: block, set site_title, site_tagline, and/or site_company_name (see deploy-from-posts.yml). Anything you omit still comes from src/config.js in the engine repo. Pagination and other blog / ui settings still come from the engine unless you fork it.
- Index is written to both:
dist/index.html(convenience)dist/page/1/index.html(consistent pagination layout)
- A simple redirecting
dist/404.htmlis generated to send users back to the index (useful for project-pages routing)