Skip to content

feat: migrate content from Contentful to EmDash - #1196

Merged
fbuireu merged 7 commits into
mainfrom
feat/migrate-to-emdash
Oct 10, 2026
Merged

fbuireu merged 7 commits into
mainfrom
feat/migrate-to-emdash

Conversation

@fbuireu

@fbuireu fbuireu commented Oct 10, 2026

Copy link
Copy Markdown
Owner

Summary

Contentful is replaced by EmDash, which runs inside the site Worker: content in the DB D1 binding, media in the MEDIA R2 binding, admin at /_emdash/admin with passkeys.

  • Content pages render per request behind the Workers cache (cacheCloudflare() + CONTENT_CACHE route rules). Every content route carries every collection tag plus the tag taxonomy tag, and EmDash purges them on publish, so publish-article.yml and the rebuild it triggered are gone. ADR 0022 supersedes ADR 0011.
  • Pages read through Astro live collections (src/live.config.ts) → application/entities loaders → CmsClientLive (in-process getEmDashCollection / getEmDashReferences, published entries only).
  • Content model in seed/seed.json. Tags become the tag taxonomy, and the Author relation gets a field on both ends, so a listing needs no reference query per article.
  • src/worker.ts wraps Astro's handler with EmDash's scheduled handler (cron every 15 min until cleanup404Log scans the 404 log on every cron tick — ~13M rows read/day at the recommended every-minute schedule emdash-cms/emdash#3748 ships) and the plugin bridge.
  • pnpm import:contentful copies every entry and asset into an empty EmDash. It assigns terms in batches of 30 to stay under D1's 100 bound variables.
  • ADR 0021 (supersedes ADR 0002); AGENTS.md, nested guides, wiki and docs-consistency test updated in the same change.

Before this can pass E2E (preview)

Previews share the development D1, which is empty until imported:

  1. CLOUDFLARE_ENV=development pnpm build && pnpm wrangler deploy --env development
  2. Run the setup wizard at the biancafiore-development workers.dev address
  3. Create a token with content:read, content:write, media:read, media:write, taxonomies:manage
  4. pnpm import:contentful --dry-run, then pnpm import:contentful, with EMDASH_URL, EMDASH_TOKEN and CONTENTFUL_* set

After merge

On the empty production database, CI deploys, smoke fails, and production rolls back to the Contentful build. That is expected. Then, at a quiet hour:

  1. pnpm wrangler versions list --env production, then pnpm wrangler rollback <version-id> --env production to promote the new version
  2. Run the setup wizard on https://biancafiore.me/_emdash/admin, with sample content unticked
  3. Import against production. Content pages answer 500 until it finishes, a few minutes

Test plan

  • pnpm verify (format, typecheck, astro check, unit + coverage)
  • E2E (preview) green once the development database is imported
  • Production smoke green after the promote + import

@fbuireu
fbuireu force-pushed the feat/migrate-to-emdash branch from 209481f to b28cb13 Compare October 10, 2026 06:23
@socket-security

socket-security Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​emdash-cms/​cloudflare@​1.2.0991007597100
Addedblurhash@​2.0.51001009185100
Addedemdash@​1.2.0891009998100

View full report

EmDash runs inside the site Worker over its D1 (content) and R2 (media)
bindings, with its admin at /_emdash/admin. Pages read content through
Astro live collections per request, behind the Workers cache, which
EmDash purges by tag on every publish (ADR 0021, ADR 0022 superseding
the prerendering of ADR 0011).

- seed/seed.json holds the content model; Tags become the tag taxonomy
  and the Author relation gets a field on both ends
- src/worker.ts adds EmDash's scheduled handler and plugin bridge
- scripts/importContentful copies every entry and asset into an empty
  EmDash (pnpm import:contentful)
- publish-article.yml is gone: a publish no longer needs a rebuild
…ited

emailDelivery is a native plugin holding email:provide and the exclusive
email:deliver hook. It hands every invitation, magic link and recovery
email to EmailClient.sendCmsEmail, the contact form's Resend client and
sender, tagged cms.
…n every minute

- emdash and @emdash-cms/cloudflare 1.2.0, which bounds the 404-log scan
  (emdash-cms/emdash#3748), so the cron goes back to every minute
- the admin ships English only (admin.locales), a smaller bundle
- an Author's social network rows carry a name beside the URL; the import
  names each after its host
… and grow the slider's markers with their slide

- each entry is current from its heading crossing the reading line until
  the next one does: two scroll-driven flags and a style query, so the
  marker scales and the label slides on a transition
- a slider with dots uses native ::scroll-markers where supported, each
  widening and turning gold as its slide crosses the centre
…llection

- an Author is a byline; job title, company and social networks are byline
  custom fields (one 'Name | URL' line per network, http(s) only, since a
  byline field cannot be a repeater) and the JSON-LD is unchanged
- the import registers the fields, creates the bylines and credits each
  Article; it now needs a token with the admin scope
- bylineCache purges the bylines cache tag on byline:afterSave and
  byline:afterDelete, which EmDash does not purge itself
…ngs and menus

- Articles and Projects render through emdash/ui's PortableText; the DTO
  prepares every block (anchors and section scopes, CDN images with blur,
  classified links, YouTube/Vimeo players, invalid and raw HTML blocks
  dropped) and the components only print it (ADR 0023, ADR 0012 amended)
- videoEmbed leaves the editorial plugin for EmDash's own blocks
- per-entry SEO on Articles, site settings and header/footer menus come
  from EmDash with today's values as the DTO's fallback; editor URLs are
  checked in the DTO
- EmDashHead/EmDashBodyStart/EmDashBodyEnd are mounted; EmDash owns the
  shared head tags and Seo.astro keeps only what it does not emit
- bylineCache holds bylines:read, without which EmDash ignores its hooks;
  emailDelivery uses hooks.email-transport:register
- frame-src allows the Vimeo player; the slider's marker rules are flat so
  lightningcss keeps their selectors
@fbuireu
fbuireu force-pushed the feat/migrate-to-emdash branch from e833e64 to d48eeb8 Compare October 10, 2026 13:29
@fbuireu
fbuireu enabled auto-merge (squash) October 10, 2026 17:34
@fbuireu
fbuireu disabled auto-merge October 10, 2026 17:34
@fbuireu
fbuireu merged commit a1d7516 into main Oct 10, 2026
33 of 40 checks passed
@fbuireu
fbuireu deleted the feat/migrate-to-emdash branch October 10, 2026 17:34
@fbuireu
fbuireu deployed to development October 10, 2026 17:34 — with GitHub Actions Active
fbuireu pushed a commit that referenced this pull request Oct 10, 2026
# [1.26.0](v1.25.9...v1.26.0) (2026-10-10)

### Features

* let editors sign in to the admin with Google ([cbf6772](cbf6772))
* migrate content from Contentful to EmDash ([#1196](#1196)) ([a1d7516](a1d7516))
@fbuireu

fbuireu commented Oct 10, 2026

Copy link
Copy Markdown
Owner Author

🎉 This PR is included in version 1.26.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

This branch was successfully deployed

1 active deployment
development — d48eeb8f Deployed Oct 10, 2026 by fbuireu via Delete the preview Worker #106
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant