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
| Content | URL 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-2hello-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 A | Entity B | Same slug? | Same path? | Collision? |
|---|---|---|---|---|
Post news (no category) | Page news | yes | both /news.html | YES ❌ |
Post 2026 in category news | Post 2026 in category events | yes | /news/2026.html vs /events/2026.html | no ✓ |
Post news (no category) | Category news | yes | /news.html vs /news/index.html | no ✓ |
Tag news | Category news | yes | tag has no URL | no ✓ |
Page news | Category news | yes | /news.html vs /news/index.html | no ✓ |
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:
- Records the old URL in
lastPublishedPathbefore the edit - Validates the new slug doesn't collide
- Renders the post at the new URL
- Uploads the new HTML
- Deletes the old HTML file from Flexweg
- 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
.htmlfiles 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
| URL | What it is |
|---|---|
/index.html | Home — auto-generated by the publisher |
/404.html | 404 fallback — auto-generated by the publisher (theme's NotFoundTemplate). Flexweg serves this on any 404. |
/menu.json | Header + footer menu data — fetched by every public page at runtime |
/posts.json | Post metadata — used by flexweg-search and theme runtime widgets |
/sitemap-index.xml | Sitemap index (when flexweg-sitemaps is enabled) |
/sitemap-<year>.xml | Yearly sitemaps |
/rss.xml | Site-wide RSS feed (when flexweg-rss is enabled) |
/<category>/<category>.xml | Per-category RSS feeds (when configured) |
/robots.txt | Robots config (when flexweg-sitemaps is enabled) |
/theme-assets/<id>.css | Active theme CSS |
/theme-assets/<id>-menu.js | Theme menu loader script |
/theme-assets/<id>-posts.js | Theme 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
- Posts — slug + category fields in detail
- Pages — top-level URLs only
- Categories and tags — category slug rules
- Stale path cleanup — what happens on slug changes