Skip to main content

URL structure

Every piece of public content gets a URL — and once published, that URL is the URL. Changing it invalidates bookmarks, breaks inbound links, and can hurt SEO. This page documents how Flexweg CMS builds URLs and how it prevents collisions.

URL patterns

ContentURL pattern
Home/index.html
Top-level page/<page-slug>.html
Post with primary category/<category-slug>/<post-slug>.html
Post without primary category/<post-slug>.html
Category archive/<category-slug>/index.html
404 fallback/404.html
Tag(no URL)

Tags never appear in URLs. They're metadata for filtering and theme display only.

The patterns are fixed. There's no support for custom permalink structures (date-based, post-id-based, hierarchical paths) like WordPress. The CMS opts for predictability over flexibility.

Slug rules

Every slug must be:

  • Lower-case ASCII letters + digits + dashes only[a-z0-9-]+
  • No leading/trailing dashes
  • No double-dashes
  • No spaces, slashes, dots, accents, special characters

The admin's slugify() enforces this on every save. If you type Hello World!, it becomes hello-world. If you type мой-пост, it becomes (something Latin-friendly via Unicode normalisation; details in src/core/slug.ts).

Validation

When you change a slug in the admin:

  • The slug field validates live as you type
  • A red error appears if the slug is invalid (illegal characters, empty)
  • A red error appears if the slug collides with another entity's URL (more on this below)
  • The Save / Publish button is disabled until both errors clear

Auto-generated slugs

For new posts / pages / categories / tags, the slug auto-generates from the title:

  • "Hello World" → hello-world
  • "10 réflexions sur l'écriture" → 10-reflexions-sur-lecriture (NFC + ASCII fold)
  • "JavaScript & TypeScript" → javascript-typescript

If the auto-generated slug collides with an existing entity, the admin appends -2, -3, etc.:

  • hello-world (taken) → hello-world-2
  • hello-world-2 (taken) → hello-world-3

So creating posts with similar titles never crashes — collisions resolve silently.

You can edit the slug after auto-generation. If you edit it to something that collides, you get the validation error.

Collision detection

The admin computes the final URL path for every entity (the actual /category/post.html etc.) and refuses any duplicate. The collision matrix:

Entity AEntity BSame slug?Same path?Collision?
Post news (no category)Page newsyesboth /news.htmlYES
Post 2026 in category newsPost 2026 in category eventsyes/news/2026.html vs /events/2026.htmlno ✓
Post news (no category)Category newsyes/news.html vs /news/index.htmlno ✓
Tag newsCategory newsyestag has no URLno ✓
Page newsCategory newsyes/news.html vs /news/index.htmlno ✓

The path-level check is what makes it work. Two slugs being equal isn't a problem — what matters is whether they'd produce the same .html file.

The admin's detectPathCollision() runs across every post, every page, every category, and every tag (for category-vs-tag uniqueness within taxonomies). See src/core/slug.ts.

Slug change → URL change

If you edit a published post's slug, its URL changes. The publisher:

  1. Records the old URL in lastPublishedPath before the edit
  2. Validates the new slug doesn't collide
  3. Renders the post at the new URL
  4. Uploads the new HTML
  5. Deletes the old HTML file from Flexweg
  6. Updates Firestore: lastPublishedPath = new URL, removes old path

If the deletion fails (transient API error), the old path is added to previousPublishedPaths[] and retried on every subsequent publish until cleanup succeeds.

This means:

  • ✓ No orphan .html files left behind
  • ✓ The new URL works immediately
  • ✗ Anyone with the old URL bookmarked sees a 404

If you care about backwards compatibility for inbound links, don't change slugs after publish. If you must, set up Cloudflare / Flexweg redirects from old → new URL.

Category change → URL change

Same as slug change, with a twist: a post moving from category news to category tech changes from /news/<slug>.html to /tech/<slug>.html. The publisher deletes the old, uploads the new. Same cleanup applies.

If you remove the primary category entirely (set to None), the URL changes from /<old-cat>/<slug>.html to /<slug>.html.

Cleanup of stale paths

Every publish runs cleanupStalePaths() which iterates [lastPublishedPath, ...previousPublishedPaths] and deleteFiles any path that isn't the new path. 404s are silent (already gone). Failures get re-recorded for the next attempt.

unpublishPost() runs the same cleanup with keepPath: "" — wipes everything.

See Stale path cleanup for the full algorithm.

Why .html extensions

Flexweg's static hosting serves files as-is. A request to /news/hello.html returns the file at that path. There's no rewrite rule to make /news/hello/ work without the .html.

You could configure Flexweg's URL rewrite (some plans support this) to drop the .html, but the CMS's slug validator + path resolver assume .html URLs. If you rewrite, you need to be careful that:

  • Sitemaps reference the rewritten URLs (they don't by default — they include .html)
  • RSS feeds reference the rewritten URLs (same issue)
  • Internal links across themes use the rewritten format

Easier: live with .html extensions. They're not pretty but they work.

Special URLs

URLWhat it is
/index.htmlHome — auto-generated by the publisher
/404.html404 fallback — auto-generated by the publisher (theme's NotFoundTemplate). Flexweg serves this on any 404.
/menu.jsonHeader + footer menu data — fetched by every public page at runtime
/posts.jsonPost metadata — used by flexweg-search and theme runtime widgets
/sitemap-index.xmlSitemap index (when flexweg-sitemaps is enabled)
/sitemap-<year>.xmlYearly sitemaps
/rss.xmlSite-wide RSS feed (when flexweg-rss is enabled)
/<category>/<category>.xmlPer-category RSS feeds (when configured)
/robots.txtRobots config (when flexweg-sitemaps is enabled)
/theme-assets/<id>.cssActive theme CSS
/theme-assets/<id>-menu.jsTheme menu loader script
/theme-assets/<id>-posts.jsTheme posts loader script
/admin/Admin SPA (you log in here)
/favicon/*Favicons (when flexweg-favicon is configured)

Multilingual URL handling

Flexweg CMS is single-language per site in v1. There's no /fr/ and /en/ URL prefix support out of the box.

For multilingual sites:

  • Run two Flexweg sites (one per language) sharing the same Firebase project, with each post duplicated per language
  • Or run one Flexweg site with a language-prefix slug convention (e.g. /en-news/ and /fr-actualites/) — but this is just slug naming, not real i18n

A future version may add native multi-language content, but it's not on the roadmap.

Continue