Problem statement
The Vercel Blob store (ocobo-blob, ~55 MB) hosting all content assets — client logos, blog/story images, team photos — lives in a personal Vercel account ("wab") while the rest of the Ocobo infrastructure has moved to the team account. The store is invisible from the team dashboard, cannot be managed collectively, and likely contains assets that no longer correspond to any active reference in the codebase or markdown content.
The content asset pipeline (posts/assets/ → upload-assets.js → blob) is what drives this store. To put it under proper team ownership, we tackle it filesystem-first: clean posts/assets/ first, then push the cleaned set to a new team-owned store.
Solution
Three sequential phases:
- Audit
posts/assets/ against actual usage — scan filesystem (153 files) and cross-reference with every markdown frontmatter/body in this repo + dynamic slug patterns consumed by the website. Produces an orphans list. Initial dry-run confirms ~25 orphans.
- Review and clean the filesystem — delete confirmed orphans from
posts/assets/, commit. The filesystem becomes the canonical, clean source of truth before anything touches the new store.
- Migrate to new team-owned store — create
website-blob under the Ocobo team account, push cleaned posts/assets/ to it, then rewrite blob URLs in markdown frontmatter (team avatars, story images, blog images) to the new hostname.
The website consumer side (ASSETS_BASE_URL constant in app/config/assets.ts, plus cleanup of obsolete migration scripts and orphan public/images/team/) is handled separately in ocobo-revops/website milestone #3.
User stories
- As a developer, I want orphaned content assets removed from
posts/assets/ before migration, so the new store only holds files that are actually in use.
- As a developer, I want an automated filesystem audit, so I can identify unused assets without manual inspection.
- As a developer, I want a dry-run deletion mode, so I can review what will be removed before committing.
- As a developer, I want the new blob store under the Ocobo team account, so the whole infra is centralised and auditable by the team.
- As a developer, I want asset pathnames preserved during migration, so frontmatter URL rewriting is a hostname-only swap.
- As a content manager, I want existing markdown URLs to keep resolving during the cutover, so there is no visual regression on live pages.
- As a developer, I want the frontmatter rewrite automated and idempotent, so it can be re-run safely.
Implementation decisions
- Source of truth = filesystem, not the blob store. The blob is a downstream artifact pushed by
upload-assets.js. Cleaning the filesystem first means the next push produces a clean store automatically.
- Audit script (
posts/scripts/audit-assets.js): scans posts/assets/**, cross-references against (a) any URL containing /content/... or /assets/... in any markdown under blog/, stories/, team/, jobs/, tools/, legal/, (b) team slug filename convention (team/{slug}.{jpg,jpeg} matching team/*.md filenames), (c) website dynamic patterns: clients/{slug}-white.png and clients/{slug}-avatar.png for every story slug + every hardcoded slug in ClientCarousel.tsx. Produces audit-orphans.json.
- Delete script (
posts/scripts/delete-orphan-assets.js): reads audit-orphans.json, dry-run by default, --confirm to execute. Operates on filesystem (deletes files + commits via git), not on blob.
- New store creation:
vercel blob create-store website-blob --scope ocobo-22231b32. Retrieve BLOB_READ_WRITE_TOKEN, add to .env.local.
- Migration: re-purpose
scripts/upload-assets.js --all against the new store (one-shot full upload of the cleaned filesystem). Decision deferred to the implementation issue.
- Frontmatter rewrite: update
BLOB_BASE_URL in scripts/update-asset-urls.js, run on all markdown in blog/, stories/, team/, jobs/, tools/.
Testing decisions
- Audit / migration scripts are Node CLI utilities — no unit tests. Correctness verified by: (a) post-audit manual review of orphans (HITL), (b) post-upload HTTP HEAD check against every new store URL (0 failures required), (c) post-cutover visual smoke test on production.
Out of scope
Notes
- Old
ocobo-blob personal-account store is decommissioned in the website follow-up milestone, after one successful production deploy on the new hostname.
- Dry-run audit (2026-05) found 25 orphans: 20 unused
-color.png/-dark.png client variants, 4 legacy firstname-only team photos, 1 .DS_Store.
Problem statement
The Vercel Blob store (
ocobo-blob, ~55 MB) hosting all content assets — client logos, blog/story images, team photos — lives in a personal Vercel account ("wab") while the rest of the Ocobo infrastructure has moved to the team account. The store is invisible from the team dashboard, cannot be managed collectively, and likely contains assets that no longer correspond to any active reference in the codebase or markdown content.The content asset pipeline (
posts/assets/→upload-assets.js→ blob) is what drives this store. To put it under proper team ownership, we tackle it filesystem-first: cleanposts/assets/first, then push the cleaned set to a new team-owned store.Solution
Three sequential phases:
posts/assets/against actual usage — scan filesystem (153 files) and cross-reference with every markdown frontmatter/body in this repo + dynamic slug patterns consumed by the website. Produces an orphans list. Initial dry-run confirms ~25 orphans.posts/assets/, commit. The filesystem becomes the canonical, clean source of truth before anything touches the new store.website-blobunder the Ocobo team account, push cleanedposts/assets/to it, then rewrite blob URLs in markdown frontmatter (team avatars, story images, blog images) to the new hostname.The website consumer side (
ASSETS_BASE_URLconstant inapp/config/assets.ts, plus cleanup of obsolete migration scripts and orphanpublic/images/team/) is handled separately inocobo-revops/websitemilestone #3.User stories
posts/assets/before migration, so the new store only holds files that are actually in use.Implementation decisions
upload-assets.js. Cleaning the filesystem first means the next push produces a clean store automatically.posts/scripts/audit-assets.js): scansposts/assets/**, cross-references against (a) any URL containing/content/...or/assets/...in any markdown underblog/,stories/,team/,jobs/,tools/,legal/, (b) team slug filename convention (team/{slug}.{jpg,jpeg}matchingteam/*.mdfilenames), (c) website dynamic patterns:clients/{slug}-white.pngandclients/{slug}-avatar.pngfor every story slug + every hardcoded slug inClientCarousel.tsx. Producesaudit-orphans.json.posts/scripts/delete-orphan-assets.js): readsaudit-orphans.json, dry-run by default,--confirmto execute. Operates on filesystem (deletes files + commits via git), not on blob.vercel blob create-store website-blob --scope ocobo-22231b32. RetrieveBLOB_READ_WRITE_TOKEN, add to.env.local.scripts/upload-assets.js --allagainst the new store (one-shot full upload of the cleaned filesystem). Decision deferred to the implementation issue.BLOB_BASE_URLinscripts/update-asset-urls.js, run on all markdown inblog/,stories/,team/,jobs/,tools/.Testing decisions
Out of scope
ocobo-revops/websitemilestone 📝 add hubspot implementation post #3.website/scripts/— see milestone 📝 add hubspot implementation post #3.website/public/images/team/(~31 MB, not referenced in app code) — see milestone 📝 add hubspot implementation post #3.Notes
ocobo-blobpersonal-account store is decommissioned in the website follow-up milestone, after one successful production deploy on the new hostname.-color.png/-dark.pngclient variants, 4 legacy firstname-only team photos, 1.DS_Store.