Talking URLs: Slug-Based Navigation

Talking URLs: Slug-Based Navigation

slugsurlsroutingseouxblazor
Fiction

Talking URLs: Slug-Based Navigation

Close-up of a keyboard, focused on individual keys Photo: Unsplash

There are things in web development you do because they're "simply right," even if they mean more work. Slug-based URLs are one of them. /stories/the-last-library is better than /stories/4291 — for users, for search engines, for anyone who shares the link. But getting it right takes more effort than you'd think.

This week I'm explaining how the slug system in OutaStory is built.

What a slug is and why it matters

A slug is the human-readable part of a URL. Instead of a database ID — which says nothing about the content — you get a descriptive word or phrase in lowercase, separated by hyphens.

For stories, that means: /stories/the-last-library instead of /stories/4291. For authors: /authors/leila-sarkari instead of /authors/77.

The difference isn't just aesthetic. Slug URLs are:

  • Shareable — you understand what awaits you just by reading the link
  • SEO-friendly — search engines use URL components as content signals
  • Stable — an ID can change through database operations, a slug doesn't
  • Linkable — authors can put their own profile link in an email signature without inserting a string of digits

The grammar of a slug

Not every string is a valid slug. We defined an explicit grammar:

  • Only lowercase letters a–z
  • Digits 0–9
  • Single hyphens as separators — no double hyphens, no leading or trailing hyphen
  • Minimum length: 2 characters
  • Maximum length: 80 characters
  • No special characters, no umlauts, no spaces

That means umlauts get converted during the automatic slug suggestion: "ü" → "ue", "ö" → "oe", "ä" → "ae", "ß" → "ss". When entering a slug manually, the form immediately rejects invalid characters.

Reserved words

Some slugs are reserved — authors can't choose them because they would collide with system routes. Examples:

  • new, edit, delete, create
  • admin, api, auth, login, logout
  • about, contact, terms, privacy

The list is checked server-side during validation. A slug on the reserved-words list fails with a clear error message — not a cryptic HTTP 409, but "This slug is reserved and cannot be used."

The draft placeholder: `draft-

Here's the detail that got asked about the most: what happens before an author has chosen their slug?

When an author creates a new story and saves the first draft, the story automatically gets a temporary slug: draft- followed by a shortened GUID. For example: draft-a3f9b2e1.

This draft slug isn't accessible to external users — stories in draft status are never public. But it lets the system always work internally with slugs, never with IDs. All internal links, all Service Bus messages, all asset paths reference the slug.

As soon as the author enters a slug in step 1 of the publish wizard and publishes the story, draft-a3f9b2e1 gets replaced by the chosen slug. All internal references are updated in a cascade.

Close-up of a typewriter key with the word "WRITE" Photo: Unsplash

Uniqueness and collision handling

Slugs must be unique — at the story level and at the author level, each separately. Two stories can't share the same slug, even if they come from different authors.

That raises the question: what happens on a collision? If an author enters "the-last-library" and that slug is already taken, the system returns an error and suggests alternatives — for example the-last-library-2 or the-last-library-by-leila.

The alternative suggestions aren't arbitrary. They follow a simple pattern: first a numeric suffix, then the author's name as a suffix. That keeps the URL as readable as possible.

Routing in Blazor

The Blazor implementation uses optional route parameters. The story page accepts /stories/{slug}, the author page /authors/{slug}. Both pages check on load whether the slug exists in the database. If not, there's a 404 page — no server exception, no blank page.

For the category tree, we applied the same principle: /category/{L0?}/{L1?}/{L2?} — a nested optional routing parameter. That works in Blazor with a bit more configuration than a single parameter, but it runs stably.

SEO implications

Slug URLs have a direct SEO effect, especially for authors who share their stories on social media. When someone posts a link and the slug contains the story's title, the link text becomes a weak but relevant signal for search engines.

On top of that: canonical URLs are easier to set with slugs. We can guarantee that /stories/the-last-library is always the canonical URL, no matter how the reader got there.

What's next?

Next week: how the monetization model is built — and why authors have the choice to publish their story completely ad-free.


Comments (0)

No comments yet.


Leave a comment

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please reload the page.