How publishing works
Publishing turns a draft post into a static HTML file uploaded to your Flexweg site. The whole pipeline runs in the browser of the admin who clicks Publish — there's no server, no build worker, no edge function.
The big picture
┌─────────────────────────────────────────────────────────────────┐
│ Admin clicks Publish │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Build PublishContext │
│ • Load all posts, pages, terms, media from Firestore │
│ • Apply patches for the post being published │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Resolve the post's URL │
│ • core/slug.ts.buildPostUrl(post, terms, settings) │
│ • Detect collisions; bail if any │
└─────────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────── ────────────────────────────────────┐
│ Render the body │
│ • core/markdown.ts: marked + DOMPurify │
│ • applyFilters("post.html.body", html, post) │
│ ↳ block transforms (columns, embeds, theme blocks…) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Render the page │
│ • Active theme's SingleTemplate inside BaseLayout │
│ • react-dom/server.renderToStaticMarkup → HTML string │
│ • Replace <meta name="x-cms-head-extra"> sentinel with output │
│ of applyFilters("page.head.extra", "", baseProps) │
│ • Replace body-end sentinel similarly │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Hash + skip if unchanged │
│ • sha256Hex(html) == post.lastPublishedHash → skip upload │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Cleanup stale paths │
│ • Delete every path in [lastPublishedPath, ...previousPaths] │
│ • that isn't the new path. 404 silent. Failures retried later. │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Upload the new HTML │
│ • flexwegApi.uploadFile(path, html) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Mark the post online │
│ • Firestore: status = 'online', publishedAt, lastPublishedPath, │
│ lastPublishedHash │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Cascade regenerations │
│ • Home page │
│ • Every category archive that lists this post │
│ • menu.json (always — slug changes might affect menu refs) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Run lifecycle actions │
│ • doAction("publish.complete", post, ctx) │
│ ↳ flexweg-sitemaps regenerates the year sitemap │
│ ↳ flexweg-rss regenerates the affected feeds │
│ ↳ flexweg-archives regenerates the touched periods │
│ ↳ flexweg-search regenerates the index │
│ ↳ (any third-party plugin subscribing to publish.complete) │
└─────────────────────────────────────────────────────────────────┘
The whole flow takes 2-10 seconds for a typical site, dominated by the cascade regenerations and lifecycle actions.
Where each step lives
| Step | Source |
|---|---|
| Pipeline orchestration | src/services/publisher.ts |
| URL resolution | src/core/slug.ts |
| Body rendering | src/core/markdown.ts |
| Theme rendering | src/core/render.tsx |
| Hash optimisation | sha256Hex in src/services/publisher.ts |
| Stale path cleanup | cleanupStalePaths in same file |
| Cascade home/archives | regenerateListings in same file |
| Lifecycle actions | doAction(...) in same file |
Why the admin renders, not a server
The static-site model trades dynamic flexibility for operational simplicity:
- No server to maintain — Flexweg only hosts files. Your monthly cost is the file storage + bandwidth.
- Public site is a CDN-friendly static bundle — fast page loads everywhere, no cold starts.
- Editing UI is a regular React app — easy to deploy, debug, extend.
The cost: every publish runs through one admin's browser. For large bulk operations (e.g. switching themes on a 5 000-post site), this means the admin tab has to stay open for several minutes. The publisher throttles uploads at 75 ms/each to avoid hammering the API.
What about preview?
There's no separate preview environment. The editor's right-side Preview (when present in your theme) renders the post through the active theme in the editor, not by uploading to Flexweg. So you can iterate on layout / content without burning publish cycles.
For a "publish to staging" workflow, run two Flexweg sites and two Firebase projects — one staging, one prod. The CMS doesn't ship a built-in flow for this; configure it via your deployment scripts.
What if a publish fails partway through?
Failures are typically network errors during the upload phase. The publisher's behaviour:
- Upload error — the post stays in
draftstate. The admin shows a toast with the error. Click Publish again to retry. - Cascade error (a category archive fails to regenerate after the post itself succeeded) — the post is
onlinebut listings are stale. Click Themes → Regenerate site → All HTML pages to recover. - Lifecycle action error (e.g. the sitemap regen fails) — the post is
onlineand listings are fresh, but the sitemap is stale. Lifecycle errors are caught + logged and never abort the surrounding publish — re-running the affected plugin's Force regenerate fixes it.