Skip to main content

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

StepSource
Pipeline orchestrationsrc/services/publisher.ts
URL resolutionsrc/core/slug.ts
Body renderingsrc/core/markdown.ts
Theme renderingsrc/core/render.tsx
Hash optimisationsha256Hex in src/services/publisher.ts
Stale path cleanupcleanupStalePaths in same file
Cascade home/archivesregenerateListings in same file
Lifecycle actionsdoAction(...) 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