Skip to main content

How Flexweg CMS works

This page walks through what actually happens when you use the CMS — when you click Publish, when a visitor opens a page, when you switch themes — so the model makes sense before you start using it.

The three storage locations

Every piece of data Flexweg CMS manages lives in one of three places:

Flexweg CMS architecture: admin SPA reading and writing both Firebase Firestore (data) and Flexweg files (published artefacts)

  • Firebase Firestore — the source of truth for everything you edit: posts (drafts and online), pages, categories, tags, media metadata, settings, user profiles, plugin configurations. The admin SPA opens live subscriptions on these collections so changes propagate instantly across admin sessions.
  • Flexweg files — the published artefacts: HTML pages your visitors load, theme CSS, media variants (resized images), generated files (sitemap.xml, rss.xml, etc.). Updated only when the admin explicitly publishes.
  • Admin SPA — has no persistent state of its own. Everything it shows comes from Firestore subscriptions or fetch() calls to the public site.

The publish flow

When you click Publish on a post:

  1. The admin reads the post + its dependencies (author, terms, hero media, settings) from Firestore.
  2. It runs the active theme's React components through react-dom/server.renderToStaticMarkup in your browser — there's no server doing this work.
  3. The result is an HTML string. The admin uploads it to Flexweg via POST /api/v1/files/upload at the post's target path (e.g. news/my-post.html).
  4. The admin runs cascade regenerations: the home page (because the latest-posts list changed), every category archive that references the post, plus any plugin-driven outputs (sitemap entries, RSS feed, search index).
  5. Hooks fire: plugins listening on publish.complete get a chance to react.
  6. The post's status flips to online in Firestore, and lastPublishedPath records where it was published — used to clean up stale files if the slug or category changes later.

The whole flow runs in your browser. The admin orchestrates by making API calls to Firestore (read content) and Flexweg (write files).

URL structure

The CMS picks the URL for each post based on its slug and primary category:

Content typeURL pattern
Post with category/<category-slug>/<post-slug>.html
Post without category/<post-slug>.html
Page (top-level)/<page-slug>.html
Category archive/<category-slug>/index.html
Home/index.html
404 fallback/404.html

Tags never appear in URLs — they're for filtering / UX only. All slugs are lower-case ASCII, dash-separated. The admin enforces this on every save.

When you change a post's slug or category, the admin deletes the old file before uploading the new one — your URLs stay tidy with no lingering 404s. See Stale path cleanup.

Themes are React components

A theme is a folder of React components — BaseLayout, HomeTemplate, SingleTemplate, CategoryTemplate, AuthorTemplate, NotFoundTemplate — plus a CSS file. The admin imports these and renders them with the post's data:

// Conceptually:
ReactDOMServer.renderToStaticMarkup(
<BaseLayout site={site} pageTitle={post.title}>
<SingleTemplate post={post} bodyHtml={renderedMarkdown} author={author} hero={heroImage} />
</BaseLayout>
)

The <base> template wraps every page (head tags, header, footer, body classes); the inner template handles the page-specific content.

There's no server-side rendering. There's no client-side hydration. The output is plain static HTML. Theme components are essentially pure functions of (props) → HTML string.

That means your CSS is the only thing styling published pages — there's no <script> tag rehydrating React on the visitor's side. Some themes ship a small companion JS file (e.g. for menu hamburgers, related-posts widgets) but it's vanilla JS, not a React bundle.

Plugins extend by hooking the publish flow

Plugins register filters (transform a value as it passes through) and actions (side effects on lifecycle events) into a registry inspired by WordPress's hook system:

api.addFilter("page.head.extra", (head, baseProps) => {
// Add a meta tag to every published page's <head>
return head + '<meta name="generator" content="Flexweg CMS" />';
});

api.addAction("publish.complete", (post, ctx) => {
// Run something after every successful publish
console.log(`Published: ${post.title}`);
});

Some hooks transform the rendered HTML (page.head.extra, page.body.end, post.html.body); some fire on lifecycle events (publish.complete, post.unpublished, post.deleted); some let you mutate menu data (menu.json.resolved). See Hooks reference for the full list.

Plugins can also contribute:

  • Editor blocks that show up in the post / page editor's inserter
  • Dashboard cards that appear on the home dashboard
  • Settings pages at /settings/plugin/<plugin-id>
  • Translations in their own i18next namespace

Dynamic menus

Header / footer menus aren't baked into every page's HTML. Instead, the admin uploads a single small /menu.json file when you save the menu config, and every published page includes a <script> that fetches that JSON at load time and fills in [data-cms-menu="header"] / [data-cms-menu="footer"] containers.

That means changing a menu item never requires re-publishing every page — the change is live the next time anyone opens any page on your site. Same for the footer.

See Menus.

What stays in Firestore vs Flexweg

DataLives inWhy
Post draftsFirestoreEditable, frequently changed
Post markdown bodyFirestoreSource of truth for re-rendering
Published HTMLFlexwegFast static serving
SettingsFirestoreEditable from admin
Plugin configsFirestoreEditable from admin
External plugin/theme registryFirestoreSurvives admin redeploys
Site language, baseUrlFirestoreEditable from admin
Theme + plugin codeBundled in admin SPAStatic at deploy time
Theme CSS (compiled)Flexweg /theme-assets/Referenced by every published page
Image variants (small/medium/large)Flexweg /media/Served as-is to browsers
Image originalsDiscardedOnly variants are kept
Search indexFlexweg /search-index.jsonLoaded by the search modal
Sitemaps + RSSFlexwegServed to crawlers and feed readers
Menu structureFlexweg /menu.jsonFetched by every published page at load time
User auth credentialsFirebase AuthStandard Firebase user store

The admin reads from Firestore, generates artefacts, and pushes them to Flexweg. Flexweg never talks to Firestore.

What this means for you

  • Backups = Firestore export + Flexweg site download. Both are independent.
  • Recovery = re-publish from Firestore content (since regenerateAll rebuilds every file).
  • Multi-site = one Firebase project per site, one Flexweg deploy per site. The admin SPA is generic.
  • Versioning = Firestore is your source of truth. If you accidentally delete a post, restore from a Firestore backup.

Continue reading