Upgrading to EmDash 0.3.0 Inside the Upgrade

Upgrading to EmDash 0.3.0: Inside the Upgrade

I leap-frogged from 0.1.1 straight to 0.3.0. Here's the full play-by-play — two sets of release notes I inherited, four small problems I had to solve, and one clean deploy.

I skipped 0.2.0 entirely. My site was running emdash@0.1.1 and the jump straight to 0.3.0 looked innocent — until npm install started throwing peer dependency errors, esbuild complained about missing tiptap modules, and a plugin I wrote myself quietly stopped working because it was missing a new capability declaration.

Here's the full play-by-play of what the upgrade actually involved, what the release notes did and didn't prepare me for, and the four small problems I had to solve to get the new version deployed.

What the releases shipped

Because I was leap-frogging 0.2.0, the new features from both minor versions landed on my site in the same breath. The ones that matter most for a blog like this:

From 0.2.0:

  • content:afterPublish / content:afterUnpublish plugin hooks — useful for webhooks and cache invalidation
  • Per-collection sitemaps, served through a <sitemapindex> at /sitemap.xml with lastmod timestamps
  • A new repeater field type for structured repeating data (feature lists, testimonials, that kind of thing)
  • A siteUrl config option that replaces the old passkeyPublicOrigin — it can resolve from environment variables, which is handy for container deployments behind a reverse proxy
  • Dashboard stats down to roughly a third of the database queries they used to run

From 0.3.0:

  • Runtime resolution of S3 storage config from S3_* env vars — build the image once, configure per environment
  • Defensive identifier validation on SQL interpolation points (a quiet but important security fix)
  • Redirect loop detection: no more ERR_TOO_MANY_REDIRECTS when editing redirects in the admin
  • Manifest version is now injected at build time instead of being hardcoded to 0.1.0
  • Admin UI loads correctly again when installed from npm (locale catalog resolution fix)

For a blog the headline feature is really the per-collection sitemaps. They're the kind of thing you forget you wanted until you notice search engines finally crawling your content properly.

Where the upgrade actually hurt

None of the breaking changes in the official notes affected me directly. The problems were all in the long tail — the kind of things nobody documents because they're second-order effects of a minor version bump.

Problem 1: plugin-forms pinned to the old emdash

My package.json still references @emdash-cms/plugin-forms@^0.1.0. That package's latest version, 0.1.1, has a peer dependency of emdash: "0.1.1" — an exact match, not a range. So npm refused to install a newer emdash alongside it.

Fix: --legacy-peer-deps. The plugin itself still runs fine against 0.3.0 at runtime; it's just the manifest that's out of date upstream. Something to revisit once upstream ships a new release.

Problem 2: Missing tiptap peer deps

emdash@0.3.0 pulls in @tiptap/extension-drag-handle for the admin editor. That package declares peer deps on @tiptap/extension-collaboration and @tiptap/y-tiptap — and none of them are marked optional. When --legacy-peer-deps silently skipped them, esbuild tried to bundle the drag-handle module and blew up with Could not resolve "@tiptap/extension-collaboration".

Fix: Install them explicitly.

npm install @tiptap/extension-collaboration@^3.22.3 @tiptap/y-tiptap@^3.0.2 --legacy-peer-deps

Problem 3: And their peer deps too

Once the tiptap modules were in place, @tiptap/y-tiptap started complaining about yjs and y-protocols/awareness. Same class of problem, one level deeper.

npm install yjs y-protocols --legacy-peer-deps

By this point the dependency graph felt like one of those matryoshka dolls where every fix reveals another one underneath.

Problem 4: My own plugin lost its hook

I maintain a local plugin, emdash-image-optimizer, that pre-generates webp variants on upload via the media:afterUpload hook. It started up under 0.3.0 but immediately logged:

[WARN] Plugin "image-optimizer" declares media:afterUpload hook without read:media capability — skipping

This is a 0.2.0-era change I hadn't noticed: hooks that touch media now require an explicit capability declaration. The plugin had been getting away with an empty capabilities: [] array because the older emdash didn't enforce it.

Fix: Update the plugin descriptor.

// plugins/image-optimizer/src/index.ts
return {
  id: "image-optimizer",
  // ...
  capabilities: ["read:media", "write:media"],
};

I also relaxed its peer range in plugins/image-optimizer/package.json from "emdash": "^0.1.0" to "emdash": ">=0.1.0" so it doesn't throw the same kind of peer warning on the next bump.

The deploy

After all of that, the actual ship was unremarkable:

  • npm run build — clean, one chunk over 500 KB but that's a Vite warning, not an error
  • wrangler deploy — 4.86 MiB uploaded, 970 KiB gzipped, worker startup 102 ms
  • curl / → 200
  • /_emdash/admin/ → 302 (redirect to login, as expected)

All six bindings still resolving: SESSION (KV), DB (D1), MEDIA (R2), IMAGES, ASSETS, LOADER.

What I'd do differently

The skipped version bit me harder than I expected. Going 0.1.1 → 0.2.0 first would have surfaced the capability check and the tiptap problem separately, instead of in one tangled session. Minor versions in pre-1.0 land aren't always "minor" in the semver-contract sense — they're more like checkpoints, and upgrading across two of them at once means inheriting every change both releases made to plugin contracts, peer deps, and runtime defaults.

The other lesson: I should treat local plugins as first-class consumers of the release notes. My image-optimizer lives in the same repo, but I'd been reading the notes looking for things that would break the *site*, not things that would break my own plugin. Both categories matter.

Small upgrade. Four problems. One clean deploy. The post you're reading is already running on it.

Keep your self updated on https://github.com/emdash-cms/emdash/releases

X:00 Y:00