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

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 draft state. 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 online but listings are stale. Click Themes → Regenerate site → All HTML pages to recover.
  • Lifecycle action error (e.g. the sitemap regen fails) — the post is online and 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.

Continue