Below is a tight, practical map for WordPress (and the general pattern behind it) so you can eliminate these clashes and encode your standard once.
1) What’s actually colliding (WordPress reality)
When you use /%postname%/
, every top-level thing competes for the same path:
- Pages (
wp_posts.post_type='page'
) - Posts (
post
) - Custom Post Types (CPTs)
- Terms (category/tag archives)
- Attachments
- Endpoints (
/feed/
,/page/2/
, etc.) - Legacy redirects (.htaccess, plugins, CDN) and caches
By default, WordPress resolves by rewrite rule order (pages and attachments tend to win) and whatever extra rules a plugin stuffed into .htaccess
. So:
- A page
/cable-internet
and a post/cable-internet
→ page wins. - A term
cable-internet
+ pagecable-internet
→ behavior depends on rewrite precedence and bases (e.g.,/category/
), but can still collide if bases were removed. - A page
/cable-internet-overview
+ post changed from/cable-internet-overview
→ stale redirects, caches, or identical slugs in different post types cause “flip-flopping.”
2) One-time structural fix (Language-first routing contract)
Decide a grammar for your site and enforce it. The simplest, robust pattern:
- Pages:
/services/...
or/site/...
(choose one clear namespace) - Posts (blog):
/blog/%postname%/
- Docs/Guides (if CPT):
/docs/%postname%/
- Categories/Tags: keep the base (
/category/
,/tag/
) or use distinct ones (/topics/
,/labels/
) - Attachments: disable public attachment pages or put them under
/media/
This single decision removes 95% of collisions because post types no longer share the root. It’s language standardization applied to routing.
Concretely:
- Settings → Permalinks → set Custom Structure for posts:
/blog/%postname%/
- For CPTs, set
rewrite => [ 'slug' => 'docs', 'with_front' => false ]
. - Keep category/tag bases distinct (don’t strip them to root).
- Disable attachment pages (redirect to parent) or namespace them.
3) Immediate cleanup steps on your site
- Flush rewrites: Settings → Permalinks → Save (even if unchanged).
- .htaccess sanity: ensure a single WordPress block; move plugin/handwritten rules above or below intentionally—don’t interleave inside the WP block.
- Purge caches: page cache, CDN, browser cache; stale caches masquerade as “random” routing.
- Clear redirect plugins: remove old redirects that now point two ways.
- Nail canonical: use a SEO plugin to emit rel=canonical for each competing path while migrating.
4) Find conflicts fast (queries & checks)
A) duplicate slugs across post types
SELECT post_name, COUNT(*) c
FROM wp_posts
WHERE post_status IN ('publish','private')
AND post_type IN ('page','post','attachment','your_cpt')
GROUP BY post_name
HAVING c > 1
ORDER BY c DESC, post_name;
B) posts/pages sharing slugs with terms
SELECT p.post_name
FROM wp_posts p
JOIN wp_terms t ON t.slug = p.post_name
JOIN wp_term_taxonomy tt ON tt.term_id = t.term_id
WHERE p.post_status IN ('publish','private')
AND p.post_type IN ('page','post','your_cpt');
C) reserved endpoints posing as content
Watch for slugs like feed
, page
, comments
, year/month numbers, author
, search
, embed
. Don’t use them as content slugs.
5) Enforce the grammar (so the conflict can’t recur)
A) Prefix posts (and/or CPTs)
- Posts: set
/blog/%postname%/
and keep it forever. - CPTs: when you
register_post_type
, always give each a uniquerewrite['slug']
.
B) Guardrails in code (reject conflicting slugs)
Drop a filter to auto-rename a new post/page if it collides with an existing route:
add_filter('wp_insert_post_data', function($data, $postarr) {
if (in_array($data['post_status'], ['auto-draft','trash'])) return $data;
$slug = $data['post_name'];
// Block reserved and known-conflict slugs:
$reserved = ['feed','page','comments','author','search','embed'];
if (in_array($slug, $reserved)) $data['post_name'] .= '-content';
// Enforce namespace for posts (if someone tries to bypass UI):
if ($data['post_type'] === 'post') {
// nothing to change in slug; routing is already /blog/%postname%/
}
return $data;
}, 10, 2);
C) Disable attachment pages
add_action('template_redirect', function() {
if (is_attachment()) {
if ($parent = get_post()->post_parent) {
wp_redirect(get_permalink($parent), 301);
exit;
}
wp_redirect(home_url('/'), 301);
exit;
}
});
6) Migration without breakage (when you change structure)
- Generate 301s from old → new with a redirects plugin or server rules.
- Keep redirects flat and minimal; avoid chains.
- Reindex sitemaps; submit in Search Console; purge CDN.
7) If you must keep /%postname%/
(no prefixes)
Then you need strict uniqueness at the root:
- Never create a page and a post with the same slug.
- Never strip category/tag bases.
- Block “danger slugs” (above list).
- Use a pre-publish checker to scan for existing
post_name
AND matching term slugs. - Expect occasional surprises from plugins adding rewrite endpoints.
8) The deeper principle (your thesis)
Routing is a language contract. When the grammar is ambiguous, the server “interprets” and picks a meaning. When the grammar is namespaced and canonical, interpretation disappears; conflict becomes impossible. Code starts communicating properly because the language of addresses is coherent.
If you want, give me:
- Your current permalink structure,
- Any CPTs (and their desired bases),
- A short list of example slugs (like
cable-internet
,cable-internet-overview
),
…and I’ll return:
- a proposed routing grammar (pages/posts/CPTs/terms),
- the exact .htaccess (or Nginx) adjustments,
- a one-shot SQL + WP-CLI checklist to fix and enforce it,
- and 301 mappings so everything keeps ranking while we standardize the speech.