URL structure and slug design for programmatic sites
On a generated site the URL is the public primary key of every page. A naming scheme chosen carelessly is paid for years later in redirects, duplication and lost signal.
Six naming rules
- Lowercase only. Some servers treat case as significant, which silently creates two URLs for one page.
- Hyphens, never underscores. A hyphen is treated as a word separator; an underscore is not.
- ASCII only. Non-ASCII characters are legal via percent-encoding but unreadable in logs, exports and shared links.
- No stop words.
/blog/how-to-build-a-sitemapbeats/blog/how-do-you-go-about-building-a-sitemap. - Three to six meaningful words. Past that, readability and share click-through both decline.
- Stable forever. A URL that changes breaks every inbound link, internal and external.
Flat or hierarchical?
| Model | Example | Upside | Downside |
|---|---|---|---|
| Flat | /blog/{slug} | No migration when taxonomy changes | No hierarchy signal in the path |
| Hierarchical | /blog/{category}/{slug} | Readable silos, natural breadcrumb | Any recategorisation forces redirects |
For programmatic projects, flat is nearly always right. The taxonomy of a generated batch shifts — subtopics merge, others split — and each shift would cost a redirect wave. Hierarchy is then carried by internal linking and by marked-up breadcrumbs rather than by the path.
A BreadcrumbList conveys hierarchy to Google independently of the URL path. That is what makes the flat model viable with no loss of signal.
Generating a stable slug
slug = transliterate(initial_title)
|> lowercase
|> replace(non_alphanumeric, "-")
|> strip(stop_words)
|> truncate(60)
|> deduplicate() # -2, -3 on collision
Generate once at creation, then freeze — even when the title changes later. A title is editorial; a slug is a technical identifier. Conflating the two means changing the primary key on every edit.
Deduplication deserves attention: two different titles can transliterate to the same slug. Without a uniqueness constraint in the database, the second page overwrites the first or fails silently.
Four traps specific to generated sites
Query parameters
A sort filter at ?sort=price&page=2 multiplies URLs for near-identical content. Either the useful combinations become clean URLs, or they are neutralised with a canonical.
Trailing slash
/blog/article and /blog/article/ are two distinct URLs. Pick one, 301 the other at the server level, once.
Homonym entities
Two entities sharing a name need disambiguation in the slug: /blog/nice-city versus /blog/nice-tool. The suffix must come from the data, never from a counter.
Dates in the path
A URL containing a year ages in public and forces an annual migration. On evergreen content, the year belongs in the title tag, never in the slug.
If the structure has to change anyway
301 every old URL, update internal links in the database rather than only in rendered output, regenerate every sitemap, and keep the old URLs in a dedicated temporary sitemap for a few weeks so the redirects are picked up faster.
Frequently asked questions
Should the category go in the URL?
Rarely on a programmatic site. Generated taxonomies shift, and every category change would force redirects. Marked-up breadcrumbs convey hierarchy without freezing the path.
Can I change a slug after publication?
You can, with a 301 redirect, but each change loses some signal and breaks links that are not updated. Freeze the slug at creation and let the title evolve.
Do shorter URLs rank better?
There is no direct ranking bonus for URL length. Short, readable URLs are shared and clicked more, which produces an indirect effect.