Slug strategy
How URLs are built from slugs, where collision detection happens, and what changes when you edit them.
The canonical implementation lives in src/core/slug.ts with comprehensive tests in slug.test.ts.
URL structure
Every public URL has the shape /<path> where <path> is computed from a small set of rules:
| Entity | URL path |
|---|---|
| Home | /index.html (always exists, even when home is a static page) |
| Single post WITH category | /<category-slug>/<post-slug>.html |
| Single post WITHOUT category | /<post-slug>.html |
| Page (top-level) | /<page-slug>.html |
| Category archive | /<category-slug>/index.html |
| Author page | /author/<author-slug>.html |
| 404 | /404.html |
| Tag pages | (none — tags don't get archive pages in v1) |
Tag pages are intentionally absent. Tags are organisational metadata for filtering / listings; they don't have their own URL hierarchy.
Slug rules
function isValidSlug(slug: string): boolean {
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);
}
Slugs MUST be:
- Lower-case — Flexweg is case-sensitive on file paths. Mixed-case slugs cause real 404s.
- ASCII — accents and non-ASCII chars are stripped by
slugify.café→cafe. - Letters + digits only, separated by single dashes — no underscores, no consecutive dashes, no leading or trailing dashes.
The admin auto-generates valid slugs from titles via slugify. User-edited slugs are validated inline; invalid slugs disable the Save button.
Collision detection
Two posts can't share an URL. Two categories can't share an URL. A post and a category CAN have the same slug — their URLs differ.
detectPathCollision(path, posts, pages, terms, excludeId?) checks the candidate URL path against every existing entity:
const collision = detectPathCollision("news/launch.html", posts, pages, terms);
// → { entityType: "post", entityId: "abc123" } | null
The check is on the final path, not the raw slug. So:
- A post with slug
news(URL:/news.html) - A category with slug
news(URL:/news/index.html)
These don't collide — the URLs are different. Both stay valid.
Same with:
- Two posts in different categories:
/news/launch.htmland/blog/launch.html. The slugs match (launch), the URLs differ. No collision.
Auto-deduplication
When a new post / page / category is created, findAvailableSlug ensures uniqueness:
findAvailableSlug("launch", posts, terms, "post");
// → "launch" if free, else "launch-2", else "launch-3", …
Used silently on auto-slug from title. User-typed slugs trigger inline collision warnings instead — admins should pick their own resolution rather than getting silently renamed.
What changes when you edit a slug
When you edit a post's slug:
- The next publish moves the file from old path → new path
- The publisher's stale-path cleanup deletes the old file
- Cascade regenerations update the home page + category archive (which list the post by URL)
menu.jsonis re-uploaded (might reference the post)- Sitemap / RSS / search index regenerate via lifecycle actions
External links to the old URL break — there's no automatic redirect (Flexweg is a static host).
When a category slug changes
Wider blast radius:
- Every post in the category has its URL change (since the URL includes the category slug)
- Every post's stale path needs cleaning up
- Listings (home, the category itself, sitemap) all regenerate
The admin doesn't auto-publish every post in a category when the category slug changes — that would be expensive on large sites. Instead, run Themes → Regenerate site → All HTML pages after the rename.