Skip to content

Publish fails with opaque D1 UNIQUE error when staged slug collides with an existing published entry #2034

Description

@hidenori-endo

Summary

Publishing a news (or any collection) entry whose staged slug collides with
another entry's already-published (slug, locale) fails with a raw D1/SQLite
error and no indication of what failed or how to fix it.

Repro

  1. Create/edit a draft entry and set its slug to a value that another
    already-published entry in the same locale already uses.

  2. Click Publish (or POST /_emdash/api/content/{collection}/{id}/publish).

  3. The request 500s. The only feedback surfaced (both in the admin UI toast
    and in the Worker logs) is:

    Content publish error: Error: D1_ERROR: UNIQUE constraint failed:
    ec_news.slug, ec_news.locale: SQLITE_CONSTRAINT (extended: SQLITE_CONSTRAINT_UNIQUE)
    

    There is no mention of the slug value, the locale, or which existing entry
    already holds it. As an editor, all you see is "failed to publish" — you
    have to go spelunking in the database to find out that a totally unrelated
    article (created via an earlier CSV/legacy import with a numeric slug like
    11) happens to already own the slug you typed.

Where this happens

ContentRepository.publish() writes the staged _slug from the draft
revision straight to the live slug column with a raw UPDATE ... SET slug = ... (see packages/core/src/database/repositories/content.ts, inside
publish(), the if (typeof revision.data._slug === "string") block). This
bypasses whatever slug-uniqueness pre-check exists for handleContentUpdate
(the comment right above handleContentPublish even calls this out: "a slug
edit is staged as _slug in the draft revision and only lands on the live
slug column here, inside repo.publish() — it never passes through
handleContentUpdate, where slug-change auto-redirects are normally
created"). So the uniqueness check that presumably guards normal slug edits
never runs on this path, and the DB constraint is the only thing that catches
it — as a generic 500.

Suggested fix

Before writing _slug to the live column in publish(), check for an
existing published row with the same (slug, locale) (excluding the current
id) and return a proper EmDashValidationError / VALIDATION_ERROR result
naming the conflicting slug — the same shape already used elsewhere in this
function for other failure modes. That turns an opaque 500 + raw SQLite
message into an actionable "this slug is already used by entry X" error the
admin UI can show inline on the slug field.

Environment

  • emdash 0.28.1
  • Cloudflare Workers + D1

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions