Make posts seed idempotent against the shared Vercel Blob store

The Vercel Blob store is shared across environments and the
@payloadcms/storage-vercel-blob plugin uploads via put() without
allowOverwrite (and exposes no option to enable it). Cover images left
under the deterministic <slug>.<ext> key by an earlier run therefore made
the prod seed abort with "This blob already exists" on the first upload.

Delete any orphan blob at that key (via @vercel/blob del, idempotent) right
before creating a new media doc, so uploads are deterministic and the seed
stays re-runnable. Existing media rows are still reused, so no duplicate
media is created.
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-15 21:18:16 -03:00
parent 28647e4760
commit bcb6c6c014
3 changed files with 23 additions and 0 deletions

View file

@ -84,6 +84,7 @@
"@types/node": "22.19.9",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vercel/blob": "2.3.1",
"@vitejs/plugin-react": "4.5.2",
"eslint": "^9.39.2",
"eslint-config-next": "15.4.11",

View file

@ -129,6 +129,9 @@ importers:
'@types/react-dom':
specifier: ^19.2.3
version: 19.2.3(@types/react@19.2.14)
'@vercel/blob':
specifier: 2.3.1
version: 2.3.1
'@vitejs/plugin-react':
specifier: 4.5.2
version: 4.5.2(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2))

View file

@ -26,6 +26,7 @@
import { config as loadEnv } from 'dotenv'
import { JSDOM } from 'jsdom'
import { convertHTMLToLexical, editorConfigFactory } from '@payloadcms/richtext-lexical'
import { del as blobDel } from '@vercel/blob'
import { getPayload } from 'payload'
// Mirror Next.js precedence (.env.local wins over .env) without clobbering a
@ -418,6 +419,24 @@ async function ensureCoverMedia(payload, { slug, coverUrl, alt }) {
if (existing.docs.length > 0) return { id: existing.docs[0].id, created: false }
const buffer = Buffer.from(await res.arrayBuffer())
// The Vercel Blob store is SHARED across environments, and the storage plugin
// (@payloadcms/storage-vercel-blob) calls `put()` without `allowOverwrite` and
// exposes no option to enable it. So a blob left behind under this deterministic
// key (`<slug>.<ext>`) by an earlier run in another environment would make the
// upload throw "This blob already exists". We have no DB media row for it here
// (checked above), so it's an orphan — delete it first so the upload is
// deterministic and this seed stays re-runnable. `del` is idempotent (no-op
// when the blob is absent).
const blobToken = process.env.BLOB_READ_WRITE_TOKEN
if (blobToken) {
try {
await blobDel(filename, { token: blobToken })
} catch (err) {
console.warn(`[seed:posts] blob pre-delete skipped for ${filename}: ${err.message}`)
}
}
const media = await payload.create({
collection: 'media',
data: { alt: alt || slug },