The build engine lives in admin/functions/build.js. It is a pure, idempotent function — every run wipes _site/ and recreates it from scratch. Running a build twice in a row produces identical output.

Triggering a build

From the admin: Click Build Site on the Dashboard.

Via API: POST /admin/api/build

Download the output: Click Download Site on the Dashboard, or GET /admin/api/build/download. This streams _site/ as a ZIP named blog-doc-site-YYYY-MM-DD.zip.

Pre-flight check

Before _site/ is touched, the build verifies the active theme contains both required templates:

  • templates/home.html
  • templates/404.html

If either is missing, the build throws immediately and _site/ is left untouched. This prevents a half-built output directory.

Build steps

The build runs these steps in order:

Step What happens
1 Load settings.json, menus.json, active-theme.json
2 Pre-flight: verify home.html and 404.html exist in the active theme
3 Initialise the LiteNode renderer
4 Wipe _site/ entirely and recreate it
5 Read all published pages from app/content/pages/
6 Read all published posts from app/content/posts/, sorted newest-first
7 Render each page with page.html_site/{url}/index.html
8 Render each post with post.html_site/blog/{slug}/index.html
9 Render paginated blog index with blog.html_site/blog/index.html, _site/blog/page/{n}/index.html
10 Render 404.html_site/404.html
11 Render taxonomy pages (categories + tags) if taxonomy.html exists
12 Render home.html_site/index.html
13 Rewrite live-server paths in all rendered .html files (/app/themes/{id}/assets/assets, /images//images/)
14 Copy theme assets → _site/assets/
15 Copy uploaded images → _site/images/
16 Generate _site/sitemap.xml
17 Generate _site/rss.xml
18 Generate _site/robots.txt and _site/data/search.json

Steps 7–12 skip draft content silently.

Output structure

_site/
├── index.html
├── 404.html
├── blog/
│   ├── index.html
│   ├── page/2/index.html
│   └── {slug}/index.html
├── categories/
│   └── {category}/
│       ├── index.html
│       └── page/2/index.html
├── tags/
│   └── {tag}/
│       ├── index.html
│       └── page/2/index.html
├── pages/{slug}/index.html        # Non-root depth-1 pages
├── {slug}/index.html              # Root pages (root: true)
├── {slug}/{child}/index.html      # Depth-2 pages
├── {slug}/{child}/{grandchild}/index.html  # Depth-3 pages
├── assets/
├── images/
├── data/
│   └── search.json
├── sitemap.xml
├── rss.xml
└── robots.txt

Generated files

sitemap.xml — Standard XML sitemap with priority values: Homepage 1.0, Blog index 0.9, Pages 0.8, Posts 0.7 (uses post date as lastmod).

rss.xml — RSS 2.0 feed of the 20 most recent published posts. Includes title, link, description, pubDate, and guid per item.

robots.txt — Allows all crawlers and includes the sitemap URL.

data/search.json — One entry per published page and post. Same file served at /data/search.json during live development. The fetch URL is identical in both environments.

isGenerateStatic

When the build engine renders a template, isGenerateStatic is true. During live development it is false. Use it to conditionally disable live-only features:

{{#if isGenerateStatic}}
<!-- nothing — static output needs no dev toolbar -->
{{#else}}
<script src="/admin/assets/js/live-reload.js"></script>
{{/if}}

Error handling

The build engine collects per-item errors without aborting. If page.html is missing and 3 pages fail to render, the build continues and reports all 3 errors in the errors array of the response. The HTTP status is 200 on full success, 500 if any errors occurred.

The stats object always reflects what actually completed, not what was attempted.

Deploying to Cloudflare Pages

Blog-Doc builds are static — they deploy cleanly to Cloudflare Pages, Netlify, GitHub Pages, or any static host.

For Cloudflare Pages, add a _redirects file to your theme's assets:

/* /index.html 200

This ensures client-side routing works correctly for clean URL paths.