fix: don't auto-redirect a slug another entry still holds - #2315
Conversation
🦋 Changeset detectedLatest commit: ca7fecc The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
A collection's url_pattern has no locale token, so every locale variant of an entry generates the same URL — and slugs are unique per (slug, locale), so a translation may legitimately share its canonical's slug. Renaming either one emitted a 301 whose source was the other's live URL. The redirect middleware runs order: "pre", so the surviving page became unreachable with no way for routing to recover. Skip the auto-redirect when another non-deleted entry in the collection still holds the old slug. Any surviving row counts, published or not: a draft that publishes later would otherwise be shadowed by the redirect. Renames that genuinely free the URL still redirect as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6938dbe to
ca7fecc
Compare
|
Rebased onto current Re-verified the bug is still live on today's Nothing on Post-rebase: full |
What does this PR do?
Fixes a slug rename taking a different entry's live page offline.
createSlugChangeRedirectbuilds the auto-redirect purely from the collection'surl_patternand the old/new slug —RedirectRepository.createAutoRedirectdoes a literalurlPattern.replace("{slug}", oldSlug). A URL pattern has no locale token, so every locale variant of an entry generates the same URL, and slugs are unique per(slug, locale)— so a translation legitimately shares its canonical's slug (the standard emdash i18n shape).Renaming either row therefore emits a 301 whose source is the other row's live URL:
esvenue atcineteca-nacional,entwin at the same slug.cinematheque→ writes/sede/cineteca-nacional→/sede/cinematheque./sede/cineteca-nacional, but the redirect middleware is registeredorder: "pre", so it intercepts before routing and the page can never recover.The mirror case is just as bad: renaming the canonical while the translation still holds the old slug redirects away from a URL the translation answers on.
The fix skips the auto-redirect when another non-deleted entry in the collection still holds the old slug. This is the same invariant
createAutoRedirectalready enforces in the other direction (it deletes redirects from the new URL because "the new URL serves live content again — any redirect from it would shadow the page"). Any surviving row counts, published or not: a draft that publishes later would otherwise be shadowed by a redirect created while it was invisible. Renames that genuinely free the URL still redirect exactly as before.I chose "is the old slug still taken?" over the narrower "skip when
locale !== defaultLocale" because the latter fixes only one direction (it leaves the canonical-rename case broken) and would also suppress legitimate redirects for sites that serve translations through Astro's locale routing while keeping one unprefixedurl_pattern. The occupancy check is correct under either routing model.Impact worth noting: this is the reason the reporting deployment keeps
url_patternNULL on every collection — accepting junk/<collection>/<slug>redirects that match no route, rather than enable a feature that can take a live page down.Found during a measured database audit of a production deployment (emdash 0.31.1).
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain. — n/a: no admin UI strings changedAI-generated code disclosure
Screenshots / test output
Both destructive directions fail on
main(the redirect source is the live canonical URL):A third test locks the positive path — a rename that genuinely frees the URL still produces the redirect — so the fix can't regress into "never redirect".
After the fix:
🤖 Generated with Claude Code