diff --git a/package.json b/package.json index 963d894c..b85d2069 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2fc8457..339e1bfc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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)) diff --git a/scripts/seed-posts.mjs b/scripts/seed-posts.mjs index 1fecdda0..8803cb82 100644 --- a/scripts/seed-posts.mjs +++ b/scripts/seed-posts.mjs @@ -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 (`.`) 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 },