Minor Changes
-
#4628
1eedf99Thanks @khoinguyenpham04! - Adds Settings > Sidebars, where each admin user hides and reorders entries in the admin navigation and sections in the content editor's settings sidebar. Every role can open it from the user menu. The layout is saved to the user's account, so it follows them to every browser, and it changes nobody else's sidebars.- Navigation entries move within their group or folder. Dashboard always stays first and visible.
admin.hiddenNavItemsstill hides entries for everyone, and those entries don't appear on the page. - The editor sidebar order applies to every collection and to new entries. Publish can move but can't be hidden.
- Reset restores a list's default and offers Undo.
The editor settings sidebar no longer shows a drag handle on each section. A Customize sidebar link after the last section opens the new page, and a pencil at the bottom of the navigation does too. Section orders that the drag handles saved in a browser are not carried over, so every user starts from the default order.
GET /_emdash/api/auth/mereturns the stored layout asadminLayout, ornull.POST /_emdash/api/auth/meaccepts{ "action": "setAdminLayout", "adminLayout": { ... } }to replace it, or"adminLayout": nullto remove it. A layout is limited to 200 IDs per list and 16 KB.Browser tests that used the editor sections' "Drag to reorder …" buttons need to change, because those buttons no longer exist. Byline reorder handles are unchanged.
- Navigation entries move within their group or folder. Dashboard always stays first and visible.
-
#4695
22b87edThanks @khoinguyenpham04! - Adds real-time co-editing to the admin editor, off by default for now. WithcoEditing: true, in collections with revisions, several people can edit an entry at the same time: each person sees the others' avatars, the field each one is in and their cursors in rich text, concurrent rich-text edits merge, and undo reverts only your own edits. A room for each entry saves everyone's edits together, with the same permissions, validation, hooks and revisions as other saves, and changes from the REST API, MCP, the CLI, the visual editor and plugins appear in open editors.Without co-editing, opening an entry takes an edit lock, and everyone else can only Open read-only or Take over. With it on, edit locks apply only in single-editor mode, which the editor uses, for example, in collections without revisions, on hosts that can't run rooms, and in browsers that can't open WebSocket connections. Admin tabs opened before co-editing is turned on keep single-editor mode until they reload. Edit with other people describes what editors see.
- Saved appears once a save holds all of your own edits. After 5 seconds without a connection, the button shows Offline: typing continues and merges on reconnect, and Preview, publishing and the other operations are disabled until then.
- Publishing, scheduling, unpublishing, discarding changes, restoring a version and moving to Trash first save everyone's edits, then run.
- Fields other than rich text keep one of two simultaneous changes, except bylines, reference fields and taxonomy terms, which keep what each person adds and removes. Turn into, dragging, deleting, wrapping, joining or indenting a block loses text someone else types inside it at the same moment.
- Edits that can't merge are saved as a recovery revision.
Public pages run no new queries, with co-editing on or off. Where rooms run, opening an entry adds a join request and a WebSocket connection. A room saves 2 seconds after the latest change, and at least every 10 seconds while people keep typing, however many people edit. Measured on Node.js with an in-memory SQLite database, saving a 2,000-word entry took 2.27 ms of CPU time at the median (3.83 ms at the 95th percentile), about two thirds of it the site's own draft save. Merging a write from outside the editor took 0.80 to 1.19 ms, and a notification with nothing to merge 0.67 ms (CPU time).
What should I do?
Nothing, unless you want co-editing. To turn it on, set the option and rebuild and deploy the site:
emdash({ coEditing: true, });
On Node.js with
@astrojs/node, in standalone or middleware mode, co-editing runs in the site's own process and the site starts as before. Check these cases first, which Co-editing on Node.js covers in detail:- A reverse proxy must forward WebSocket upgrades on
/_emdash/api/content/{collection}/{id}/room. Otherwise each browser session falls back to single-editor mode after three failed connections, and the server logs a[co-editing]warning. - Several processes on one database: route each room path to one process, for example by hashing the path, or leave co-editing off.
node:clusterworkers, including PM2's cluster mode, keep single-editor mode.- Another WebSocket handler on the same server must leave room paths alone. Replace a
wsserver created with{ server },express-wsor@fastify/websocketwith awsserver created withnoServer: truethat handles only your paths, or leave co-editing off. - On Node.js 22.16 to 22.20, 23, and 24.0 to 24.8, a server whose only
upgradelistener is EmDash's answers upgrade requests on other paths, such asUpgrade: h2c, with404. Node.js 22.21 and later, and 24.9 and later, serve them as before. - While rooms are open, EmDash handles
SIGTERMandSIGINTand stops the process up to 5 seconds later. Give process managers a kill timeout over 5 seconds, such as PM2'skill_timeout, which defaults to 1.6 seconds.
On Cloudflare, sites created from this release's templates already have the binding and the room exports, so they need only
coEditing: true. For an existing site, also add the binding and theexportsentry towrangler.jsonc, as Co-editing on Cloudflare shows:A Worker that uses the
migrationsarray, which can't be combined withexports, adds{ "tag": "emdash-rooms", "new_sqlite_classes": ["EmDashRoom"] }to it instead. Then re-export the room classes from the Worker entry point:import handler, { createScheduledHandler, EmDashRoom, EmDashRoomSite, PluginBridge, } from "@emdash-cms/cloudflare/worker"; export { EmDashRoom, EmDashRoomSite, PluginBridge }; export default { ...handler, scheduled: createScheduledHandler(), } satisfies ExportedHandler;
A Worker with its own
fetchcallshandleRoomUpgrade(request, env)first and returns its response when it isn'tnull. A site without the binding keeps single-editor mode.Changes for API clients and plugins
These apply only on sites with co-editing on.
- While people co-edit an entry, it has no edit lock: writes that got
409 ENTRY_LOCKEDland and merge into the editor. Writers that send_revcan get409 CONFLICTmore often, because the room saves every few seconds, and MCP publish, unpublish, schedule and discard conflict while the room holds unsaved edits. - The lock route's
POSTdeletes user locks while a room holds the entry, instead of granting one. - A room saves as one user, the actor, but a save can hold several people's edits. A hook that checks the actor's role before accepting content should check everyone in
editedBy. In a room,content:beforeSavehooks get only the fields a save changed, as they do for visual-editor saves. - While a room is open, editor-draft requests skip the
baseRevisioncheck, and the editor refuses a plugin's patch only when a field it changes was edited after the capture.
These apply on every site, with co-editing on or off:
PUT /_emdash/api/content/{collection}/{id}takes an optionalexpectedobject with thebylinesand reference selections (references, every page of a field) that a change was based on, as a read returned them.POST /_emdash/api/content/{collection}/{id}/terms/{taxonomy}takes an optionalexpectedlist of term ids. When a stored list differs, nothing is written and the request fails with409 LIST_CONFLICT: read the entry again, apply the change to the list it holds now, and retry. Requests withoutexpectedwork as before.POST /_emdash/api/content/{collection}/{id}/roomjoins an entry's room. It needs a signed-in session rather than an API token, and answers{ "mode": "single-editor" }unless the site runs co-editing.- A migration adds the
_emdash_roomsand_emdash_room_updatestables, which hold co-editing rooms. Site transfer never copies them. GETandPOST /_emdash/api/content/{collection}/{id}/lockresponses include aroomfield, which istrueonly while a co-editing room holds the entry.
-
#4641
007f71cThanks @swissky! - Adds type checking foremdash()options inastro.config.mjs. A value of the wrong type, such asmcp: "yes",maxUploadSize: "10MB"ortoolbar: "always", now stopsastro dev,astro buildandastro syncwith one error that lists each invalid option by its path, for example→ at mcp. Before, these values loaded without an error and failed later or were silently ignored. Adapter, plugin and provider descriptors are checked only for their outer shape, andregistry,migrations,middleware,admin.localesandadmin.hiddenNavItemskeep their existing errors.An unrecognized top-level option still loads, but EmDash now logs a warning. When the name is close to a known option, the warning suggests it, so
sitUrlpoints tositeUrl.What should I do?
If the config fails to load after upgrading, fix each option named in the error, for example change
mcp: "true"tomcp: true. The check can't be turned off. Configurations that match the documented option types are not affected. -
#4730
a17a4b3Thanks @emdash-autofix! - Adds support forpublishedAt,createdAt, andupdatedAtas a collection'sdateField, so editors can see and sort by publication dates without duplicating system timestamps in a custom field. Choose Date column source in the existing content type's settings, or setdateFieldthrough the collection update API or a seed file.Collections without a configured
dateFieldstill show and sort by last-updated. Entries with a missing or invalid configured date display their last-updated date, while sorting uses the configured source. -
#3307
98e0924Thanks @swissky! - Adds aformatsprop to theImagecomponent fromemdash/ui, so sites can serve modern image formats with a fallback.List the formats in order of preference. With more than one entry, the component renders a
<picture>element: the earlier entries become<source>elements and the last entry becomes the fallback<img>. WithoutfallbackFormat, a single entry renders one<img>in that format.<Image image={post.data.featured_image} width={1200} height={675} formats={["avif", "webp"]} />
The optional
fallbackFormatprop sets the format of the<img>and keeps everyformatsentry as a<source>. Thequalityprop applies to every format the component renders.formatsonly applies to images optimized by the site's image service; images from external media providers keep the provider's own URLs. Withoutformats, the output is unchanged. -
#4213
caca761Thanks @emdashbot! - Reduces CPU usage during image upload confirmation by generating placeholders from small browser thumbnails instead of decoding full-resolution originals. For browser-decodable images, the admin now sends thumbnails fitting a 64-pixel box with both signed and direct uploads, and the visual-editing toolbar sends one with every direct upload. This also enables blurhash and dominant-color placeholders for browser-decodable formats such as WebP and AVIF, which previously had none. New uploads and imports no longer generate placeholders for transparent images, including PNGs, so placeholder backgrounds do not show through transparent areas. Existing media keeps its stored placeholders. Image dimensions, upload validation, content hashing, and responsive image variants remain unchanged.Adds the
imagePlaceholdersconfiguration option, defaulting totrueon every runtime, including Cloudflare Workers. This is one site-wide switch for every upload and import: setting it tofalseskips all blurhash and dominant-color generation, including from client thumbnails. WordPress imports, MCP uploads, plugin uploads, and custom clients without thumbnails still decode originals when placeholders are enabled, so sites with constrained CPU budgets may choose to turn this switch off.Custom clients can send an optional
{ thumbnail: { contentType, data } }inPOST /_emdash/api/media/{id}/confirm. Confirm thumbnails accept JPEG or PNG, at most 32,768 base64 characters and 4,096 decoded pixels. Unusable thumbnails omit the placeholder rather than falling back to decoding the original. Thumbnails also work when originals exceed the 8 MiB download cap; supply the originalwidthandheightto record dimensions in that case. -
#4728
73c9b70Thanks @emdash-autofix! - Adds$media.fileto seed files so local images can be imported without serving them over HTTP or disabling URL SSRF protection. Use{ "$media": { "file": "assets/photo.jpg" } }with an image next to the seed atassets/photo.jpg.The CLI and Astro setup resolve files relative to the selected seed file's directory. Programmatic callers must supply
mediaBasePathand a storage adapter. Supply exactly one non-nullurlorfile; null values are treated as absent, and references that supply both sources are skipped. File paths must stay within that directory, including symlink targets. Local files require a Node.js filesystem at seed-apply time and are not bundled for deployment; Astro setup requires the seed directory at its build-time path.skipMediaDownloadskips local files rather than creating external URLs for them. -
#4729
006cfc0Thanks @emdash-autofix! - Adds theno-schedulingcollection support flag to disable creating or changing schedules in the content editor, calendar, and scheduling API. Scheduling remains enabled by default, and existing schedules continue to run and can be removed.To disable scheduling, add
no-schedulingto the collection'ssupportsin a seed file, or update an existing collection throughPUT /_emdash/api/schema/collections/{slug}or theschema_update_collectionMCP tool. For example, a collection with drafts and revisions can use:{ "supports": ["drafts", "revisions", "no-scheduling"] }Preserve the collection's other support values when updating its list. There is no Content Type editor checkbox for this flag, but saving in that editor preserves it.
If you previously omitted
schedulingexpecting scheduling to be disabled, addno-schedulinginstead. The legacyschedulingvalue is deprecated and has no effect; it remains accepted for compatibility and can be removed from seed files. If both values are present,no-schedulingtakes precedence. Sites that want to keep scheduling enabled do not need to change anything.WordPress imports into a collection with
no-schedulingkeep future-dated posts as drafts and report a scheduling error for each such post. -
#4668
4d4165cThanks @khoinguyenpham04! - Adds recovery revisions, which keep a copy of an editor's unsaved fields in an entry's revision history without changing the entry. Save one withPOST /_emdash/api/content/{collection}/{id}/revisionsand a body of{ "data": { ... }, "slug": "..." }, whereslugmay benull.- The route needs a signed-in session, not an API token, and edit access to the entry. It works for entries in Trash, accepts up to 1.9 MB of fields, and responds with the new revision's
id. - The revision is stored as sent, without field validation or
content:beforeSave, and changes neither the draft nor the published version. Restoring it saves its fields and slug as an edit by the restoring user, so validation andcontent:beforeSaverun then. - Site export leaves recovery revisions out.
- The route needs a signed-in session, not an API token, and edit access to the entry. It works for entries in Trash, accepts up to 1.9 MB of fields, and responds with the new revision's
-
#4637
c9696afThanks @khoinguyenpham04! - Fixes autosave deleting versions saved with Save, and names everyone who edited each version in revision history.- An autosave replaces the previous version only when that version is also an autosave. A version saved with Save, or one EmDash creates itself, such as the draft copy an unpublish leaves, stays in Revisions.
- Each version names everyone who edited it since the version before it, such as "Ana, Ben, and 1 other person", and marks its changes in a neutral color when several people edited it. The publish confirmation names everyone who edited the entry since the published version.
GET /_emdash/api/content/{collection}/{id}/revisionsand the MCPrevision_listtool returneditedByon each version: the people who edited it, latest first, as{ id, name }, withnullfor an account that no longer exists.nullmeans the version names only itsauthor.GET /_emdash/api/content/{collection}/{id}/revisions/editors?since={revisionId}returns everyone who edited the entry in the versions newer thansince, newest first and once each, in the same shape. Withoutsince, it returns everyone who edited the entry. It requirescontent:read_drafts.content:beforeSaveandcontent:afterSaveevents includeeditedBy, the IDs of the users whose edits the save holds, latest first. A save with anactorlists only that user. A hook that checks the actor's role before accepting content should check everyone ineditedBy.
A migration adds two nullable columns,
kindandedited_by, to therevisionstable. Existing versions are not backfilled: they name only their author, and autosave never replaces them. Site transfer leaves both columns out, so transferred versions also name only their author. Once a site has run the migration, earlier EmDash versions can't export it or import into it with site transfer. -
#3716
284fa80Thanks @swissky! - Adds a staging mode that keeps a site out of search engines until it goes live. Sites created through setup, whether in the setup wizard or by a script that calls the setup API, start in staging:- Pages that render
<EmDashHead>get anoindex, nofollowrobots tag, which overrides SEO panel and plugin robots values. Templates without<EmDashHead>don't get the tag. /robots.txtkeeps its usual content so crawlers can read the tag, and isn't cached while the site is in staging.- The admin header shows a Staging badge.
To go live, an administrator selects Go live in Settings > General > Site Status, sends
{ "staging": false }to the settings API, or uses the MCPsettings_updatetool. Switch back to staging hides the site again. Staging is visibility only: visitors can still open every page.Existing sites are not affected: a site without the setting is live. Site transfer and
emdash export-seeddon't copy the staging status. - Pages that render
-
#2156
25574daThanks @swissky! - Adds a built-in SMTP email provider. It delivers through any standard SMTP server, such as Brevo, Office 365, Fastmail, Amazon SES or a self-hosted Postfix, on Node and on Cloudflare Workers with thenodejs_compatflag.Configure
Choose SMTP under Settings → Email, or set environment variables:
- Required:
EMAIL_SMTP_HOST,EMAIL_SMTP_USERandEMAIL_SMTP_PASS. - Optional:
EMAIL_SMTP_PORT(587 when empty),EMAIL_SMTP_SECURE,EMAIL_SMTP_FROM,EMAIL_SMTP_FROM_NAME,EMAIL_SMTP_FROM_EMAILandEMAIL_SMTP_REPLY_TO.
Without a sender address, the username is used as the sender, so the admin requires a sender email when the username is not an email address (for example an Amazon SES or SendGrid key). Settings saved in the admin take precedence over environment variables and apply without a restart. Saving them requires
EMDASH_ENCRYPTION_KEY, which encrypts the password in the database. Changing the host or username requires entering the password again.Port 465 uses implicit TLS and port 587 uses STARTTLS. The provider verifies the server certificate, never signs in before TLS is established, and signs in with
AUTH PLAINorAUTH LOGIN, whichever the server offers. Port 25 is refused. Invalid environment variables are ignored with a warning in the server log.Provider selection
Settings → Email now has a provider dropdown that lists SMTP and any installed email plugins. When its environment variables are set, SMTP delivers email unless an email plugin is active or another provider is selected. Without them, SMTP is used only after you select it. A site with a single email plugin still selects that plugin automatically. In
astro dev, the console provider is used only when no other email provider is active, and that choice is no longer saved to the database.Send test email now shows the SMTP server's error, for example a rejected login, instead of a generic failure.
API
GET /_emdash/api/settings/emailalso reports the SMTP settings, without the password. The newPUT /_emdash/api/settings/emailsaves SMTP settings together with the selected provider, or selects a plugin provider.PUTwith{ "provider": "smtp" }and no settings selects SMTP from its saved settings or environment variables. Sending a test email withPOST /_emdash/api/settings/emailreturns SMTP delivery errors with status 502. - Required:
Patch Changes
-
#3066
c108e7bThanks @zaochih! - Accepts BMP images in the Media Library. Uploads through the admin, the media API, and the MCPmedia_uploadtool no longer fail withFile type not allowed. BMP files show as thumbnails in the upload dialog, are included in the "Images" preset of the allowed file types editor, and are displayed in the browser instead of downloaded. Fields with their ownallowedMimeTypescan list them as.bmp. -
#3707
118dcf9Thanks @nozamdavid! - Fixes the admin manifest so the logo selected in Site Settings appears in the admin branding when no logo is configured directly inadmin.logo. -
#4631
32eab5cThanks @swissky! - Reduces database queries on pages that render the same byline more than once in a single request, such as severalgetEmDashCollection()lists or agetEmDashEntry()page with a related-posts list, when byline custom fields are defined. Each byline's custom field values are now loaded once per request instead of once per list, including when several lists render at the same time. -
#4237
40e4400Thanks @danielmlr! - Fixes the commandemdash loginsuggests after "Device Flow is not available for this instance.", such as on a site that uses Cloudflare Access for sign-in. The suggestedemdash --tokencommand failed with "Unknown command"; the hint now showsemdash schema list --url <url> --token <token>, and when login got past Cloudflare Access with--headerflags,EMDASH_HEADERSor cloudflared, it also says how to pass that access with each command. -
#4340
e79a7ffThanks @Chiji1108! - FixesEmDashClientso migration scripts can keep an entry's original dates:create()acceptspublishedAtandcreatedAt, andupdate()andpublish()acceptpublishedAt. The content API already accepted these fields for users withcontent:publish_any, but the client's types rejected them andupdate()andpublish()did not send them. -
#4339
7169073Thanks @Chiji1108! - Fixes theMediaItemtype returned byEmDashClientso it describes the media items the API sends.storageKey,status,contentHash,blurhash,dominantColorandauthorIdare now typed, andmediaList()andmediaUpload()return items with their fileurl.Code that reads these items may need small type changes; runtime values are unchanged:
keyandupdatedAtare removed. The API never returned them, so they were alwaysundefined. Readitem.storageKey, or useitem.urlto link to the file.size,width,height,altandcaptionare typedT | nullinstead of optionalT, because the API sendsnullwhen they are unset. Useitem.alt ?? undefinedwhere astring | undefinedis expected.
-
#4582
2abe403Thanks @swissky! - Fixes@emdash-cms/cloudflareinstalling its own copy ofemdashwhen the site has a differentemdashversion. The adapter now declaresemdashas a peer dependency with its exact matching version, so a site always has a singleemdash.Keep both packages on the same version and update them together:
npm install emdash@latest @emdash-cms/cloudflare@latest
A project with mismatched versions now fails
npm installwith anERESOLVEerror instead of installing a second copy. pnpm and Yarn print a peer dependency warning, andastro buildandastro devwarn as before. -
#4603
e8ba1b8Thanks @dmythro! - Fixes the Columns and Cover blocks dropping everything inside them except text. Images, buttons, galleries, embeds, tables and other blocks inside a column or a cover now render on the site, with the same components EmDash'sPortableTextuses elsewhere. -
#4575
39a9ccbThanks @disney2002! - Fixes dark mode styling acrossCommentsandCommentFormcomponents under.dark:- Updates
Commentsto use a dark border color ([#374151](https://github.com/emdash-cms/emdash/issues/374151), overridable via--ec-comment-border-color-dark) for separators and reaction buttons. - Inverts
.ec-comment-form-submitin dark mode (#f3f4f6background,[#111827](https://github.com/emdash-cms/emdash/issues/111827)text, overridable via--ec-form-submit-bg-darkand--ec-form-submit-color-dark) for high contrast against dark backgrounds. - Fixes
.ec-comment-user-inforetaining a light border color in dark mode by applyingborder-color: var(--ec-form-user-border-dark, #4b5563).
- Updates
-
#4716
b766773Thanks @emdash-autofix! - Fixes the preview path for Cloudflare Stream videos stuck at0:00in the Media Library detail panel. The panel uses Stream's hosted player instead of the browser's native HLS/DASH support. Stream preview URLs ending in/watchare converted to the embeddable/iframeURL. Media providers can supply an optionalplayerUrlfor admin video previews; providers without one keep native video playback.If you use Astro's content security policy, allow the Stream player host in
frame-srcso the preview can load. EmDash's default admin policy already allows HTTPS frames. -
#4098
ddc4e82Thanks @marcodangelo! - Fixes generating a taxonomy term slug on Cloudflare D1 when the slug derived from the term's name is already taken and 49 characters or longer. The lookup failed withLIKE or GLOB pattern too complexinstead of adding a numeric suffix. -
#4651
638cbafThanks @viktor-kryzhanovsky! - Fixes sites using anobjectCachebackend serving the previous version of an entry after a content update under concurrent read load, where the stale entry could keep being served until the next write to the same collection or until the cache TTL expired. -
#4445
678a0c2Thanks @danielmlr! - Fixes slug changes on draft entries so they no longer create a redirect from the draft's old URL or remove the redirect that already answers at its new URL. -
#4483
7d48e95Thanks @emdash-autofix! - Updates the Zod dependency from 4.5.4 to 4.6.7 to address the reported high-severitynpm auditwarning for the older version. Upgrade EmDash and any separately installed plugin tooling to receive the updated dependency; refreshing a lockfile alone cannot replace the previous exact pin.Oversized repeater arrays now report the array-length error without validating their rows. Arrays within the configured limit still report individual row errors.
-
#4696
18844a7Thanks @swissky! - Fixes embed blocks whose HTML contains an iframe from a host other than YouTube or Vimeo, such as Google My Maps embeds imported from WordPress, or whose HTML is empty after sanitizing. They rendered as an empty frame or box; they now link to the embed URL, and any other HTML in the embed still renders. -
#3600
8418defThanks @itaides! - Exports the email hook types (EmailMessage,EmailDeliverEvent,EmailBeforeSendEvent,EmailAfterSendEventand their handler types) fromemdash, so a plugin that providesemail:deliveror hooksemail:beforeSend/email:afterSendcan type its handlers without copying the definitions. -
#4619
fbddae0Thanks @swissky! - Returns a generic503 Service Unavailable(withRetry-After: 60andCache-Control: private, no-store) instead of401 Authentication failedwhen external authentication such as Cloudflare Access succeeds but the local user lookup, provisioning, or sync fails, for example because database migrations have not been applied. Rejected identities still return 401. -
#4727
f5aab79Thanks @emdash-autofix! - Removes a separate schema-version query whengetEmDashEntry()andgetEmDashCollection()render bylines, saving a round trip to D1 and other remote databases. Changes to custom byline fields remain visible across server instances on the next request. -
#4508
5137be5Thanks @emdash-autofix! - Fixes invite creation returning “Admin privileges required” for signed-in admins when the API URL has a trailing slash. Invite acceptance remains available without signing in. -
#4172
679ad6cThanks @cyface! - When using auto migration mode, cold starts on an already-migrated database no longer spend a separate database round trip on the migration check: on backends with a coalescing dialect (D1, Durable Object SQL), the applied-migration count is read in the same batch as the other startup reads. -
#4733
612569bThanks @emdash-autofix! - Fixes admin browser tabs to show the current page and Site Title, falling back to configured admin branding and then EmDash when Site Title is empty. Content editor tabs include the collection type and entry title and update during client-side navigation. -
#4460
b331cf3Thanks @emdash-autofix! - Fixes stale comments on route-cached pages after approved submissions, moderation, or deletion, including bulk actions. Comment actions that leave approved comments unchanged do not purge cached pages. -
#4734
786fd09Thanks @emdash-autofix! - Fixes content lists showing the original title and field values after saving changes to unpublished drafts in collections with revisions. Entries with a live revision continue to show their published values until the draft is published. -
#4644
97aa98aThanks @emdash-autofix! - Fixes auth emails, test emails, domain-change notices, and passkey prompts ignoring the title saved in Settings > General. They now prefer that title, falling back to the title recorded during setup when no Settings title is set. Plugins also receive this title when the runtime initializes; an already-running runtime needs to reload before plugins see a changed title. -
#4531
f8462afThanks @emdash-autofix! - Fixes widget API mutations purging cached routes and returning before the object-cache epoch write has settled. Widget edits now wait for the epoch-write attempt before purging routes. This orders the epoch write only: isolates that revalidate through the site-wide change marker can still reuse an old widget epoch until the background marker update becomes visible. Workers KV propagation delays and isolate-local epoch reuse also remain; this does not guarantee immediate cross-isolate freshness.Code that writes widget tables directly can await
invalidateWidgetObjectCache({ waitForPersist: true })before purging routes. The same option is available oninvalidateObjectCache(namespace, { waitForPersist: true }). Existing calls without options remain non-blocking. Epoch writes for all namespaces now use the configured cache timeout, logging failures without failing the mutation; a timed-out backend write may still complete later. Change-marker updates and retries remain background work. -
#4705
55273d7Thanks @emdash-autofix! - Fixes CLI authentication against local dev servers configured with an external auth provider, such as Cloudflare Access, so commands can use dev bypass without a stored token. -
#4607
53b4f32Thanks @emdash-autofix! - Fixes Markdown imports through the client API, CLI, and MCP to preserve nested inline formatting, links, and list depth. GFM pipe tables now import as native table blocks and export as Markdown tables, including cell formatting and alignment. Tables with merged cells or other layouts that GFM cannot represent remain preserved in opaque block comments. Headings containing hard line breaks are also exported as opaque blocks to preserve their text and formatting.Markdown imports now treat a single newline as a soft wrap within one paragraph; separate paragraphs with a blank line. Reads escape literal inline punctuation (for example,
snake_casebecomessnake\_case), use*text*for italics instead of_text_, and write hard line breaks as a trailing backslash (\).Unlinked images in ordinary paragraphs become native image blocks, splitting surrounding text into separate paragraphs. Images inside list items, blockquotes, or links remain inline Markdown text so their containing list, quote, or link is preserved.
-
#4484
55964d5Thanks @emdash-autofix! - Fixes the visual editor toolbar to detect and publish unpublished changes for every annotated entry on the page, rather than only the first entry on load or the last entry saved. If an entry fails to publish, other pending entries are still published and the page stays open so failed entries can be retried without republishing entries that already succeeded. -
#4417
235a11aThanks @emdash-autofix! - Fixes seed imports assigning taxonomy terms from another locale when slugs are shared across translations. Taxonomy references now resolve in the content entry's locale. When no term with that slug exists in the entry's locale, the import logs a warning and skips that assignment instead of attaching a term from another locale; add the term in that locale to keep the assignment.Reapplying a seed in update mode also preserves taxonomy assignments contributed by each content translation instead of letting later translations erase earlier ones.
Existing assignments are not repaired automatically. To repair seeded entries already attached to a wrong-locale term, reapply the seed with
--on-conflict update. This also overwrites other seeded records with the seed's values, so review the seed before reapplying it. -
#4703
4ccc279Thanks @emdash-autofix! - Fixes concurrent CLI sign-ins, token refreshes, and sign-outs overwriting credentials for other EmDash sites. -
#4095
ab2c40bThanks @emdashbot! - Fixes plugin storage-index synchronization so a fresh runtime cannot serve state-changing plugin routes before the declared indexes are materialized. These routes return503 STORAGE_INDEXES_NOT_READYif synchronization is incomplete. After correcting the underlying conflict, a subsequent write request or scheduled maintenance pass retries synchronization without a process restart. GET and HEAD plugin routes do not trigger index synchronization.When stored indexes match the plugin's declarations, a cold runtime checks them once without reissuing index statements. Updating or reinstalling a runtime-installed plugin rechecks its declarations before its next state-changing route runs. While a conflict remains, each subsequent write request retries the plugin's full index synchronization and returns 503. Correct the conflicting data before retrying to avoid repeated index work.
-
#4476
e30df1eThanks @emdash-autofix! - Fixes stale runtimes and background cron jobs surviving Node.jsastro devrestarts, such as after editingastro.config.mjs, which could causeVite module runner has been closederrors. The next request now initializes a fresh runtime. -
#4542
7159edaThanks @emdash-autofix! - Fixes content creation on Cloudflare D1 when a long generated slug is already taken, allowing repeated titles to receive numeric slug suffixes instead of failing withCONTENT_CREATE_ERROR. -
#4353
9699645Thanks @emdash-autofix! - Fixes intermittent Cloudflare Workers startup failures withCannot access 'VALID_HOOK_NAMES_SET' before initializationwhen standard-format plugins are configured in the Astro integration. -
#4598
dd4ae19Thanks @onetogregorio! - Fixes the Portable Text link mark dropping atitlestored on the link, so<PortableText>now renders it as the link'stitleattribute (shown by browsers on hover). Links without a title render as before. -
#4563
82eb778Thanks @saariuslystoned! - Fixes<LiveSearch>keyboard navigation after a result has focus: Down and Up keep moving through the results (wrapping at either end), and Escape closes the results and returns focus to the search input. Previously both keys stopped working once the first result was focused. -
#4694
3f9c77bThanks @swissky! - Fixes the MCPsettings_updatetool reporting success forseo.robotsTxtandseo.disallowAiTrainingon sites with their ownsrc/pages/robots.txt. That file replaces EmDash's/robots.txt, so these settings never took effect. The tool now rejects changes to them withROBOTS_TXT_OVERRIDDENand stores nothing. Sending the stored values back unchanged, or clearing them with""orfalse, still works. -
#4668
4d4165cThanks @khoinguyenpham04! - Fixes MCPcontent_updatereporting a plain conflict after it had already saved field changes. When it saves the fields and the publish or unpublish requested withstatusthen conflicts, theCONFLICTerror now says the fields were saved and returns their new_revindetails._rev, so an agent can retry the status change without sending the fields again. -
#4326
e25d2eeThanks @khoinguyenpham04! - Fixes Used in missing rich text images and videos that refer to a library file by its/_emdash/api/media/file/URL instead of its media ID. Markdown images written through MCP or the CLI are stored this way, so files placed by agents and scripts looked unused, even right after media usage tracking was turned on. These references are now indexed. On sites that already track media usage, collections with rich text or blocks fields are marked for reindexing after the upgrade, and Used in reports their index as stale. To reindex them, an administrator opens Settings > Media usage tracking and keeps the page open until the status reads Ready.Saving rich text through the REST API, MCP, or the CLI now also stores the media ID (
asset._ref) for these blocks when the URL matches a file in the media library, as images placed in the editor already have. -
#4454
5a9f1bdThanks @khoinguyenpham04! - Fixes deleting a menu item on SQLite, libSQL, and Cloudflare D1 so the items nested under it are deleted too, as they already were on PostgreSQL. Before, the nested items stayed behind: the site showed them as top-level menu items, but the menu editor no longer listed them, so they could not be edited or removed.When a menu item has nested items, the menu editor's delete confirmation now says they will be deleted too.
Upgrading moves every menu item whose parent is missing from its menu to the top level, where the site already shows it, so the menu editor lists it again.
What should I do?
After upgrading, review your menus in the admin. Items left behind by an earlier delete now appear at the top level, where your site was already showing them. Delete any you no longer want.
-
#4491
6331f6bThanks @emdash-autofix! - Fixes uploads into image and file fields inside blocks ignoring the nested field'sallowedMimeTypes. Allowed Word documents and other types outside the default upload list can now be uploaded directly from a block's media picker.API clients uploading into a block field can send
blockType(the block type slug) andblockVersion(the block's version number) with the compositefieldId(<outerFieldId>:<blockKey>:<fieldSlug>) toPOST /_emdash/api/mediaorPOST /_emdash/api/media/upload-url. Use the database ID of the collection's blocks field forouterFieldId, and sendblockVersionas a string in multipart requests or a number in upload-URL JSON requests. Without the block context, the default upload allowlist still applies. -
#4289
19c4f28Thanks @Antonio-Bennett! - Fixes OAuth sign-in on Node adapters ignoring provider credentials (EMDASH_OAUTH_*and provider*_CLIENT_ID,*_CLIENT_SECRETand*_TENANT_IDvalues) set at runtime, for example with container environment variables. Sign-in failed withprovider_not_configuredunless the credentials were also present when the site was built, and credentials present at build time were written into the server bundle.On Node, OAuth credentials must now be in the server's process environment at runtime; values baked in from
.envat build time are no longer used. If you only set them in a.envfile, load it when starting the server (for examplenode --env-file=.env ./dist/server/entry.mjs) or set them in your host's environment; otherwise sign-in fails withprovider_not_configured.astro devstill falls back to.env. If your server build output was shared or stored, rotate any client secret that was present at build time. -
#4079
fc44f40Thanks @marcodangelo! - Fixes the object cache reading one epoch key per namespace on every render oncerevalidatehas passed. On Workers KV withkvCache(), a page that reads several collections, menus, bylines, and taxonomies now usually reads one change-marker key to revalidate all of them, instead of one KV read per namespace. Isolates still pick up a change within KV propagation plusrevalidate. After deploying, the saving starts with the first content, menu, or settings change, because older releases do not write the marker. Comment changes don't rewrite the marker, and the comments epoch is always read directly. Withrevalidate: 0, every query still reads its epochs directly. -
#3019
df0f952Thanks @marcin-misiewicz-pl! - Fixes the JSON-LD that core emits on public pages, which published nodes nothing could reference.BlogPosting, itspublisher, andWebSitehad no@id, so an Organization graph describing the site — from a plugin or written into a template — stood beside the article's publisher as a second, competing organisation instead of merging with it. Article pages carried two organisations, and the fuller one was not the article's publisher.Each of the three nodes now carries an
@id:<canonical>#article,<origin>/#organization, and<origin>/#website. The publisher keeps its@typeandname, so a site publishing no Organization graph of its own is unaffected.Sites already emitting an Organization graph should confirm its
@idis<origin>/#organization— with the slash before the fragment.https://example.com#organizationis a different IRI, and a mismatch produces two organisations rather than one. -
#3266
60fef17Thanks @eisenbruch! - Speeds upgetEmDashCollection()listings on SQLite and D1 that filter by two or more taxonomies and no content fields. The listing now starts from the taxonomy with the fewest entries instead of the one written first inwhere, so a broad taxonomy listed first no longer slows it down. -
#4511
b9fc5f2Thanks @danielmlr! - Fixes preview links in Playground sessions: the admin's Preview button now shows the saved draft instead of the published page. -
#2817
dfbb9fcThanks @scottbuscemi! - Fixes config-registered plugins showing their identifier instead of their display name and omitting their description in the admin Plugins list. -
#4593
5605cecThanks @swissky! - Fixesastro devreloading the admin on the first visit after a cold start. Plugin admin pages that importemdash/plugin-utilsno longer load against stale pre-bundled modules from before the reload. -
#4284
9aa2259Thanks @devondragon! - FixesgetSiteSettings()andgetSiteSetting()ignoring the storagepublicUrl. On sites that serve media from an R2 custom domain or S3 CDN, the site logo, favicon, and default social image now use that URL instead of/_emdash/api/media/file/. Admin and REST API responses keep the internal route.Also fixes
<Media>throwing "Local media provider requires database connection" when rendering media from the media library. It now renders local media with the storagepublicUrlwhen one is configured. -
#4591
b528fc0Thanks @swissky! - Fixes Archives and Recent Posts widget links andhreflangalternate URLs on sites configured with an Astrobase. They now include the base path instead of pointing to the site root. -
#4708
752cbcdThanks @emdash-autofix! - Fixes site transfer imports failing withTRANSFER_TARGET_NOT_EMPTYafter an interruption while removing starter collections created during setup. Resuming the import now continues that cleanup instead of treating the collection as existing content.Settings → Transfer now identifies collections whose deletion failed and explains how to retry them before importing. Deletions that are still in progress do not block a new import; the importer advances their cleanup.
What should I do?
If your import was interrupted, reopen Settings → Transfer to continue it, or run
emdash site import resume <operation-id>. If the import or a collection deletion failed, follow the guide's retry and recovery steps before starting another import. -
#4742
b8886d9Thanks @danielmlr! - Fixes WXR imports publishing password-protected WordPress posts for anyone to read on the imported site. The admin import andemdash import wordpressnow bring in a post with a WordPress password as a draft, whatever its WordPress status, and list each one on the Import Complete screen and in the CLI output.Imports through the EmDash Exporter plugin are unchanged: the exporter does not send post passwords, so a published, password-protected post still arrives published.
What should I do?
If you imported a WXR file before this release, or import through the EmDash Exporter, find the entries that WordPress marks as "Password protected" on its Posts, Pages and custom post type screens, and unpublish them in EmDash if they should stay private.
-
#3016
c095458Thanks @jakevis! - Fixesemdash schema delete --forceso it can delete a collection that still has content. The flag only skipped the confirmation prompt and was never sent to the API, which then refused with "Use force: true to delete".EmDashClient.deleteCollectionaccepts{ force: true }for the same purpose. -
#3916
8f397e0Thanks @eisenbruch! - FixesgetEmDashCollection()withwhere: { id: [...] }on SQLite walking the whole collection onceANALYZEhas run on a database with trashed entries. The requested ids are now looked up by primary key; other list queries are unchanged. -
#4403
b436669Thanks @emdash-autofix! - Fixes re-enabling a plugin reporting success when itsplugin:activatehook fails. Failed activation now returns an enable error and leaves the plugin inactive, with its routes, hooks, sandbox host access, and cron tasks disabled. -
#4557
4cb8305Thanks @emdash-autofix! - Fixes Registry plugin installs and updates failing withAGGREGATOR_RECORD_MISMATCHwhen a newer package profile is awaiting listing review. Releases still have to match the publisher's signed records, and verification and consent continue to use the publisher's current signed release policy. While a newer profile is awaiting review, installed and updated plugins take their name and description from the approved registry listing. -
#4539
0c57c94Thanks @danielmlr! - Fixes a rare case where a redirect that took too long to create could still be added after a newer redirect change had started, once the redirect write lock had been recreated. Such a redirect is now rejected. -
#4366
56c801cThanks @cameronapak! - Fixes redirect creation and editing failing with "Another redirect change is in progress" when the redirect write lock row is missing. Redirect writes now recreate the missing row automatically while preserving active locks. -
#4615
aef09aeThanks @swissky! - Fixes redirects whose source path contains non-ASCII characters, such as/stitek/domácí-zvířata. These rules never matched before and visitors got a 404. Exact and pattern redirects now match whether the source was entered as raw Unicode or percent-encoded in either letter case, with no changes needed to existing rules. Non-ASCII characters in a redirect destination are percent-encoded in theLocationheader instead of failing the request. Loop detection, both when saving a redirect and on the Redirects page, treats raw and percent-encoded spellings of a path as the same path. -
#4061
1f46f0dThanks @eisenbruch! - Fixes API tokens being unable to manage redirects without theadminscope. Reading redirects now takescontent:readand creating, updating or deleting one takessettings:manage. Reading the 404 log, which holds visitor IP addresses, takessettings:read, and clearing or pruning it takessettings:manage. The token owner's role is still checked. -
#4668
4d4165cThanks @khoinguyenpham04! - Fixesctx.content.delete()in sandboxed plugins, on Cloudflare and with@emdash-cms/sandbox-workerd, so it moves entries to Trash as the admin does. Sites with anobjectCacheno longer keep serving the trashed entry from the cache, and the entry'supdated_atand_revno longer change. -
#4782
9db2de8Thanks @eisenbruch! - Fixes a scheduled entry that publishing refuses staying overdue forever. When the scheduler's publish fails with a validation error or a slug conflict, the entry is now unscheduled and the dashboard lists it with the error message, as it already does when a plugin rejects a scheduled publish. Before, the error went only to the server log, the scheduler retried it on every run, and the calendar said scheduled publishing might not be running.ScheduledPolicyRejection.pluginIdis now optional, and the record gains an optionalcode: a record written for a refused publish has nopluginIdand carries the publish error'scodeinstead. Code that readsemdash:scheduled-policy-rejection:*records, or narrows withisScheduledPolicyRejection, should handle a missingpluginId. -
#4560
1357172Thanks @marcodangelo! - Shortens each cron tick on sites with several collections, most noticeably on Cloudflare D1, where every query is a network round trip: the scheduled-publishing sweep now skips collections with nothing due instead of querying each of them twice. -
#4715
2c2a1e8Thanks @emdash-autofix! - Fixes plugin storageuniqueIndexesrejecting duplicate field values in unrelated plugins and collections. Uniqueness now applies only to the collection that declares it, while indexed lookups remain available. A database migration repairs existing tracked unique indexes, including those left by plugins removed from configuration; no manual index cleanup is needed. Unique index names also keep case-distinct collections and fields separate on SQLite/D1 and avoid PostgreSQL truncation collisions. Existing regular lookup indexes retain their short names; tracked names longer than 63 characters may be rebuilt on the next storage-index sync, triggered by a write or scheduled task.Sites with plugin storage unique indexes need a maintenance window for migration
095_plugin_storage_unique_scope. Do not apply it in a pre-deploy migration job while older builds still serve traffic or scheduled work: they can recreate the non-partial constraints. Stop and drain old Node.js processes before migrating. On Workers, route traffic to maintenance and drain old invocations, deploy the upgraded build incheckmode, then migrate and reopen traffic. Follow the platform-specific steps in the upgrade guide.If a previous index-name collision or manual index removal allowed duplicates in a declared unique field, the migration stops without deleting data or dropping legacy constraints. Resolve the duplicate values in the collection named by the error, then retry the migration.
-
#4534
532e78bThanks @khoinguyenpham04! - Fixes the default SEO title ignoring a collection'stitleField. When an entry's SEO title is empty,getSeoMeta()now takes the page title from the collection's title field for entries fromgetEmDashEntry()andresolveEmDashPath(), and falls back to thetitlefield when the collection sets notitleFieldor the entry leaves that field empty. The SEO Title placeholder in the editor shows the same value. Before, both read only thetitlefield, so a collection that keeps its titles in aheadlinefield got an empty page title.What should I do?
If a collection's
titleFieldnames a field other thantitleand the collection also has atitlefield, its pages now take their title from thetitleFieldvalue. To keep using thetitlefield, pass it as the default title:const seo = getSeoMeta(post, { defaultTitle: post.data.title });
-
#4553
5845694Thanks @emdash-autofix! - Fixes theEmbedcomponent rendering unrelated links as YouTube or Vimeo players when they contain a video URL in their path or query string, or use a lookalike hostname. Video detection now recognizes onlyyoutube.com,www.youtube.com,m.youtube.com,youtu.be,vimeo.com, andwww.vimeo.com; other hosts remain links unless the block supplies HTML or an explicit audio/video provider. -
#4697
c9dffb7Thanks @swissky! - Fixes slug-change redirects for collections whose URL pattern ends in/, such as/whats-on/{slug}/. The automatic redirect now points to the URL with its trailing slash. When an entry's slug changes, existing redirects to or from its old and new URLs are matched with or without the trailing slash, so old URLs no longer take an extra hop and a redirect at the new URL no longer sends visitors away. Disabled redirects and 410 or 451 rules at the old URL are left unchanged, and the old URL keeps that rule instead of getting a new redirect. When an entry is published at a URL that an automatic slug-change redirect still points away from, that redirect is now removed in either form. -
#4596
2f64c3cThanks @eisenbruch! - Reduces the code a site evaluates on the first request of each new server instance. Importing anything fromemdash, such asgetEmDashCollection, no longer forces the API schemas, the WordPress importer, the seed applier and other admin-only code to load before a page renders. They load when an admin, API or import request first uses them. Sites need no changes. -
#4610
34a8e5aThanks @swissky! - Fixes backup downloads and preview snapshots failing on Cloudflare D1 for sites with a lot of content or revision history. From November 4, 2026, D1 rejects query results larger than 50 MB. Backups and snapshots now stay under that limit and contain the same data as before. -
#4478
3a13512Thanks @emdash-autofix! - Fixes the public Image component and Portable Text image and gallery blocks emitting duplicatesrcsetURLs when Astro’s image service returns the source URL unchanged. This includes the public Image component’sformats/<picture>output for light and dark variants. These images now render without generatedsrcsetorsizes; resizing still requires a working image service. -
#4510
1cf644eThanks @emdash-autofix! - Replaces the Change domain error "does not serve this site yet" with "Could not verify that … serves this site" when the server cannot verify a domain, including Cloudflare Workers Custom Domains. The dialog now explains that a Worker may be unable to reach its own domain and asks you to confirm the new address shows your site in a browser before using it without the check. -
#4389
fac4008Thanks @danielmlr! - Fixes Revisions missing the content an entry started with, so editors can compare against it or restore it after editing. In collections with revisions, the first save of an entry that has no revision yet now keeps its starting content as its oldest revision, whether that save comes from the admin, the API, MCP or a plugin. This covers new and duplicated entries, translations, imported posts, and existing entries that haven't been saved since they were created. -
#4589
c54ca68Thanks @emdash-autofix! - Fixes category, tag, and search widget URLs on sites configured with an Astrobase. Built-in sitemap routes now initialize correctly under the base instead of returning 500, and sitemap URLs and robots.txt sitemap references include the base path. -
#4401
e21e01eThanks @emdash-autofix! - Fixes responsive table widths in the admin editor with Tiptap 3.32.0. Gives both editors' textboxes accessible names for screen readers.Pins the editor's Tiptap dependencies to exactly 3.32.0 so installations use the version covered by the editor tests. Code-block highlighting keeps its existing lowlight extension version. Sites or plugins that share Tiptap dependencies with EmDash should align them to 3.32.0 to avoid installing multiple copies.
Links applied at an empty caret now end when you type a space, following Tiptap's updated behavior. In the visual editor, typing a space at the end of an existing link also ends the link. Text typed after that space is not part of the link.
-
#4726
f298866Thanks @emdash-autofix! - Fixes unnecessary session-store reads on public pages for visitors without a session cookie, reducing anonymous render latency on sites using remote session storage such as Cloudflare KV. Respects custom Astro session cookie names when identifying authenticated requests in the main middleware and on public routes. -
#4088
dafb551Thanks @mateusz-zadorozny! - Avoids an extra query when a listed entry has no terms in a taxonomy.getTermsForEntries()andgetEntryTerms()now answer from the terms already loaded with the content query, so a blog list with an untagged post no longer queries its tags again. -
#4581
3d569abThanks @emdash-autofix! - Fixes taxonomywherefilters with long term-slug lists failing with D1's bound-parameter limit and returning an empty collection result with an error, including queries that combine taxonomy and field filters. -
#4711
6ee8a5fThanks @emdash-autofix! - Fixes CLI dev authentication creatingdev@emdash.localbefore first-run setup and blocking the setup wizard with “Admin user already exists”. Dev authentication now requires completed setup; finish the wizard first or explicitly skip it underastro devwith/_emdash/api/setup/dev-bypass?content=0.If an earlier version has already left your development site blocked by this error, opening
/_emdash/api/setup/dev-bypass?content=0underastro devcompletes setup using the existing dev account without adding sample content, bylines, or taxonomy terms. It still applies the seed's schema and structure. This skips the wizard rather than replacing the dev account with a personal account. -
#4699
d541993Thanks @emdash-autofix! - Fixes WordPress import URL rewriting skipping Portable Text images and links on PostgreSQL. Also handles image and file fields returned as parsed JSON, while preserving existing behavior for text-backed media fields. -
#4526
34a6816Thanks @emdash-autofix! - Fixes passkey login returning “Not authenticated” before the browser can request a passkey when middleware adds trailing slashes to API URLs. Public API routes, including login options and health checks, now accept either spelling while keeping private routes protected and preserving CSRF checks. -
#4482
88a61f8Thanks @emdash-autofix! - Fixes stylesheets imported by a plugin'sadminEntryappearing on public pages in production builds. Plugin admin styles now load with the admin app without adding render-blocking stylesheet requests for public visitors. -
#4358
0008289Thanks @emdash-autofix! - Fixes config-declared sandboxed plugins, including those running withsandbox: false, skippingplugin:installandplugin:activateon first boot. These hooks now run for plugins without a recorded state, including on the first start after upgrading, so activation hooks can set up recurring tasks without an admin disable/re-enable cycle.If a sandbox stops before setup finishes,
plugin:installcan run again on the next boot. Make install hooks safe to repeat when settings or plugin data already exist.createPluginRuntimeTestHost()also runs install → activate when creating a host, before test fixtures are added. A failure in the plugin's own lifecycle hook disables it in the test host, so later actions will not invoke its hooks. Tests that explicitly callactions.plugin.activate()should account for the initial activation.What should I do?
Make install hooks set only missing values. If a plugin hook failed, fix the hook and re-enable the plugin from the Plugins page to run
plugin:activateagain. -
#4398
a7cf38fThanks @emdash-autofix! - Fixes/_emdash/api/searchsilently returning no results on Node.js builds whose SQLite lacks FTS5. Search and suggestion requests return503 SEARCH_UNAVAILABLEwith an explanation; enabling or rebuilding search reports the same cause. MCP search errors also use theSEARCH_UNAVAILABLEcode. Programmatic search functions throw the exportedSearchUnavailableError. Server-rendered search pages that do not catch it return an error until FTS5 is available. Catch this error to display an unavailable message. After switching to a build with FTS5 support, enable search for any collections whose setup failed. -
#4415
76c1936Thanks @emdash-autofix! - Fixes content previews after a saved slug change to open and render the draft at its new slug, without changing the published URL until publication. -
#4662
222658cThanks @emdash-autofix! - Fixes Portable Text type exports fromemdash/uiso TypeScript consumers can resolve the types and reject invalid values such as a number assigned toPortableTextBlock.The
<PortableText>component's props are now type-checked too, so invalidvalue,components, orlistNestingModearguments may report errors after upgrading. Undefined content remains supported and renders as empty content. -
#4702
c2792d5Thanks @emdash-autofix! - Fixes plugin route handlers receivingnullclient IP and geo metadata on Cloudflare when invoked through MCP tools, content editor panels, or editor actions. -
#4660
064b566Thanks @emdash-autofix! - Fixesemdash/ui,emdash/ui/search,emdash/ui/comments, andemdash/ui/webmcp-searchimports so TypeScript uses package declarations instead of checking EmDash source under your project's compiler settings, including when using Astro's TypeScript content mapper. Astro continues to compile the components from source when building your site.Plain
tscnow checks component props, including required props andBlocksrenderer types. Calls that previously relied on loosely typed.astroimports may report invalid props after upgrading. -
#4632
96945d3Thanks @khoinguyenpham04! - UpdatesportableTextToProsemirrorso a video block's unsetprovider,widthandheightattributes arenull, as in the admin editor, instead ofundefined. -
#4277
26528bbThanks @emdash-autofix! - Fixesctx.kvvalues disappearing when a plugin with the same ID moves betweenplugins: []andsandboxed: []. This storage migration requires a coordinated upgrade; it cannot be deployed as a rolling upgrade.Both sandbox runners share the in-process KV store. A database migration copies existing non-
settings:sandbox values and revisions without overwriting in-process values, warns about conflicting plugin IDs and keys, and retains the original sandbox rows for recovery. Uninstalling a marketplace or registry plugin with data deletion removes its shared KV values and settings; failed-install rollback preserves existing data, including partial writes made by an installation hook before it fails. Plugin-test KV fixtures and inspection use the shared store; use the settings inspection API forsettings:keys.What should I do?
- Back up the database.
- Stop all application instances and plugin jobs that write to it.
- Upgrade
emdashand your sandbox runner together, then build the site without starting it. - Run
pnpm emdash migrateagainst the reviewed database target. - Resolve conflicting KV copies before plugins can write again.
- Start or deploy the upgraded site.
Do not rely on default
automigrations for this upgrade: they start plugins immediately after migrating, leaving no conflict-review window. Mixed package versions are not automatically rejected. See the plugin upgrade guide for target selection, recovery queries, and rollback instructions. Downgrading packages alone hides post-upgrade KV writes; restoring the pre-upgrade database backup discards all site changes made since it was taken. -
#4709
a107170Thanks @emdash-autofix! - Fixes WordPress WXR and plugin imports discarding dates on posts imported as drafts. Publishing those posts now keeps the imported date and date-based permalink. This also applies to genuine WordPress drafts with a source date, including the local-date fallback, rather than dating them at first publication in EmDash. Posts scheduled for a future date still get their publication date from the final schedule. WordPress missed schedules remain drafts and keep their past source date when published. -
#4604
c77bfb1Thanks @dmythro! - Fixes the Categories, Tags, Recent Posts and Archives widgets showing visitors the default language on translated pages. They now list the terms, posts and months of the page's own locale, as signed-in editors already saw, and link under that locale, such as/en/category/news,/en/archives/2025/01and/en/posts/museum-news. A Recent PostsurlTemplategets the locale prefix too, after Astro'sbasewhen the template starts with it, unless the template already has the prefix.Recent Posts links to translated posts change from
/posts/en/museum-newsto/en/posts/museum-news, the shape Astro's locale routing and the EmDash templates use. -
#3765
776c95eThanks @maikunari! - Adds theemdash import wordpresscommand that the WordPress migration guide refers to. It converts a WordPress WXR export into JSON files on disk. This release also fixesemdash --versionandemdash --helpreportingv0.0.0instead of the installed version.Analyze the export first, review the generated
migration-config.json, then convert the content:npx emdash import wordpress export.xml npx emdash import wordpress export.xml --execute
Converted entries, a redirect map, downloaded attachments, and a progress file are written to
./wordpress-importunless you pass--output-dir. After an interruption, run the command again with--resumeto skip entries and attachments that were already converted. The analysis step does not overwrite an existingmigration-config.jsonunless you pass--force. Attachments keep their path belowwp-content/uploadsinside the media directory, and scheduled WordPress posts are converted with the statusscheduledand ascheduledAtdate.--skip-medialeaves attachments out, and--dry-runreports the files a step would write. The command does not write to an EmDash database; the admin importer remains the way to import WordPress content into a site. -
#4601
44daa08Thanks @dmythro! - Fixes WordPress WXR file imports creating a new guest byline for the same author every 30 posts. An author who is not mapped to a user now gets one byline for the whole file import, so their posts are no longer split betweenauthor,author-2,author-3and so on. Imports through the WordPress plugin connector (EmDash Exporter) are not yet covered by this fix. -
#4592
746db9fThanks @danielmlr! - Fixes WordPress menu links to custom post type entries disappearing from the site after an import through the EmDash Exporter plugin. Imported menu items now link to the collection the content was imported into.What should I do?
Menus imported before this release are not repaired. Links to post types with a hyphen in their name, such as
team-member, and to post types the import renames, such asevent,faqandtestimonial, can be missing from the site, together with their submenu items. If you imported through the exporter API (/_emdash/api/import/wordpress-plugin/execute) with your ownpostTypeMappings, links to any post type you mapped to a different collection can be missing too,postandpageincluded.In the menu editor, an affected link's badge names a collection your content isn't in, such as
team-memberinstead ofteam_member. Add each affected link again with Add Content. Deleting a link also deletes its submenu items, so first make the new link the Parent of the old link's submenu items. Then delete the old link. -
#4666
652d957Thanks @dmythro! - Fixes WordPress export file (WXR) imports leaving out menus. The admin said the WXR path imports menus, but the import created none, and the review screen didn't list them. The review screen now lists a file's menus by their names in WordPress, with the switch that exporter imports already have, and the import creates them with their nesting and with links to the imported pages and posts. An item without its own label shows the linked entry's title, as in WordPress. On a site translated with Polylang or WPML, each menu takes the language of the pages it links to. An item that links to a post type archive links to the archive of the collection that the post type was imported into. Items that link to a category or tag archive, to content the import left out, or to one of several pages that share a slug in the same collection and language are skipped and counted in the import result. So is a Polylang language switcher, which only Polylang can turn into language links. A menu that already exists under the same name and language is left as it is. -
Updated dependencies [
c108e7b,7bce592,3d4faaf,76f0f53,76f0f53,5a8c620,968a23e,1eedf99,b42176d,8ce12b7,e34b8fe,c2a6857,52b430c,76f0f53,84e173f,8d0250c,6d5a10d,e737608,73158ea,0d5c65d,a499750,22b87ed,b766773,213904f,2237d52,7d48e95,cada545,d891305,612569b,a17a4b3,cdfd98d,c38a5d5,871cbfa,46e0a54,4df65eb,caca761,faeca6c,2bd4a39,252934b,5a9f1bd,6331f6b,935cdc7,d6c56fc,752cbcd,b8886d9,40c7657,88e00fe,5652829,830578b,006cfc0,4e95f4f,d4d149d,5bcccb8,d3e3021,c9696af,34fc9e0,22b87ed,faeca6c,8b484aa,372bafa,9db2de8,532e78b,089716d,c0930c3,a3fc555,284fa80,25574da,1cf644e,96945d3,a5223a9,e21e01e,eefe351,65b1679,bb15813,98d16d8,93ad617,34ff31a,a3da98d,652d957]:- @emdash-cms/admin@1.5.0
- @emdash-cms/blocks@1.5.0
- @emdash-cms/gutenberg-to-portable-text@1.5.0
- @emdash-cms/auth@1.5.0
- @emdash-cms/plugin-types@0.6.1
- @emdash-cms/registry-lexicons@0.7.1
- @emdash-cms/registry-verification@0.3.5
- @emdash-cms/registry-client@0.7.2