github emdash-cms/emdash emdash@1.3.0

Minor Changes

  • #4117 d9705ad Thanks @khoinguyenpham04! - Updates the visual-editing rich text editor's selection toolbar and slash menu to match the admin editor's.

    Selecting text now shows the admin's toolbar: a Turn into menu, link, bold, italic, underline, strikethrough, inline code, and a More menu with subscript, superscript, alignment, and Clear formatting. With the caret in a link, a bar shows where it goes, with buttons to edit or remove it, and clicking a link no longer opens it in a new tab. A typed bare domain such as example.com links to https://example.com, and a link the editor can't use is refused with a message.

    The slash menu now groups its commands, shows each one's Markdown shortcut and description, ranks matches as you type (/bl finds Bullet List), and finds Heading 4 to 6 by name. Headings 4 to 6 now show at their own size while editing instead of as Heading 1. Both menus follow the system's light or dark theme and stay readable in dark mode.

  • #4137 9b11a80 Thanks @khoinguyenpham04! - Adds an image picker and an image toolbar to the visual-editing rich text editor.

    Typing /image now opens a picker below the line instead of a full-screen dialog. It shows the media library nine images a page, newest first, with a tab for each media provider the site has set up. One click, or the arrow keys and Enter, inserts an image.

    The picker no longer uploads images. To use a new image, upload it to the media library in the admin first, then choose it with /image. When the library is empty, the picker links to the media library, which opens in a new tab.

    Images now show as they publish, with their alignment and a caption field right underneath. Selecting one shows a toolbar to replace it, edit its alt text, or delete it. Replacing an image keeps its caption and takes the new image's alt text.

    Typing /image now opens a picker only in the editor it was typed in, not in every editor on the page, and an inserted image no longer gets its file name as its alt text. An image inserted from a line inside a quote or list now goes after the quote or list; saving used to drop an image inside one.

  • #4068 4fc0ced Thanks @danielmlr! - Adds emdash migrate --take-over-lock <id>, which applies pending migrations under a stuck D1 migration lock without freeing it first. Releasing the lock with --release-lock and then running emdash migrate let a Worker in auto mode take the free lock in between, and the apply failed with a lock error.

    pnpm emdash migrate --take-over-lock 1788264000000

    Like --release-lock, it asks you to confirm the target, or needs --expected-target-fingerprint in a non-interactive shell.

  • #4136 83fe26d Thanks @khoinguyenpham04! - Adds readable revision history to the content editor. Selecting a version in the Revisions panel shows it in place, read-only, with added text highlighted and removed text struck through in the color of the person who saved it. The selected version lists its changes in plain words, such as "Changed the title", and selecting one scrolls to it. Restore moves to a bar at the bottom of the editor, and a version that matches an earlier one is marked "Restored from" with that version's date. Save and Publish pause while a version is open. The panel no longer shows raw JSON.

    The revisions list API (GET /_emdash/api/content/{collection}/{id}/revisions) and the MCP revision_list tool include each revision's author as { id, name }, or null when no author was recorded or the account no longer exists, so editors without access to user management can see who saved each version.

    ContentEditor's onRevisionRestored callback can now return a promise. The restore stays pending until it settles, and the restore dialog shows the error if it rejects. When a host passes this callback, the restored version stays on screen until item carries the restored draft, and the writer can't go back to editing before then. The editor still refreshes the entry's content and revisions queries, so a host that reads item from the entry's content query needs no change. Other hosts must reload item from the callback.

  • #3623 49319eb Thanks @swissky! - Adds a Disallow AI training option to Settings > SEO. When it is on, /robots.txt disallows the AI training crawlers and opt-out tokens Amazonbot, Applebot-Extended, Bytespider, CCBot, ClaudeBot, Google-Extended, GPTBot, and meta-externalagent, and adds a Content-Signal: search=yes, ai-train=no line. Search engine access stays as your other rules define it. The rules are added to both the default and a custom robots.txt. The option is also available as seo.disallowAiTraining in the settings API and the MCP settings_update tool. It is off by default, so existing robots.txt output is unchanged.

Patch Changes

  • #4034 d7f2b8a Thanks @danielmlr! - Fixes emdash whoami failing on sites behind Cloudflare Access: it now sends the headers from --header and EMDASH_HEADERS and reads EMDASH_URL, like the other commands. Without a token on localhost, it now signs in through the dev bypass and reports the dev user, and exits 1 when no site answers or the site isn't running under astro dev, so scripts can use it as a reachability check.

  • #3955 291e0f8 Thanks @khoinguyenpham04! - Fixes <CommentForm> making right-to-left pages (<html dir="rtl">) about 10,000 pixels wider, with a horizontal scrollbar. This happened on every such page in Firefox, and in Chrome and Safari when the form or an element around it is positioned, for example with position: relative. On right-to-left pages, <Comments> now also indents threaded replies from the right, and the separator before a signed-in commenter's email in <CommentForm> no longer touches the email.

  • #3907 bf046ca Thanks @emdashbot! - Reduces getComments() to a single database query when the approved comment list fits on one page.

    Pages rendering comments previously ran a COUNT query followed by the list query, even when the result was not truncated. getComments() now fetches the list first and derives the total from the returned rows whenever cursor pagination indicates there are no more pages, falling back to COUNT only for posts that exceed the page limit. Cache misses on sites without an object cache benefit most.

  • #4133 ae6c1a4 Thanks @ascorbic! - Fixes Contributors being unable to save changes to their own drafts. A Contributor could create an entry, but the next save, the entry lock, and setting categories or tags returned 403. Contributors can now edit their own entries while they are drafts, in the admin, the REST API, and the MCP content_update tool. They still cannot edit entries once they are published or scheduled, or publish them.

    The new content:edit_own_drafts permission grants this and is held by Contributor and every higher role. No other role's permissions change.

  • #3937 9ba9753 Thanks @swissky! - Fixes slower page loads for logged-out visitors on D1 with session: "auto" or "primary-first" and no coalesce. A session runs a request's queries one at a time, so EmDash's preload of site settings, menus, widget areas, and taxonomy terms for HTML pages delayed the page's own first query and added queries the page might never use. In that setup EmDash no longer preloads them; those queries run only when the page uses them. Sites with coalesce: true keep the preload, batched into one round trip.

    New Cloudflare sites from the templates now use d1({ binding: "DB", session: "auto", coalesce: true }), so reads issued together share one D1 round trip, with or without read replication. To get the same on an existing site, set session: "auto" (or "primary-first") and coalesce: true in its d1() call; coalesce has no effect without a session. coalesce is experimental: a read and a write started together may run write-first, so await a read before a write that must not affect it.

  • #4171 3116dbe Thanks @ascorbic! - Fixes the admin dashboard for users without content:read_drafts, such as Subscribers. Recent activity and collection counts now include only published entries, matching what these users can open elsewhere in the admin, and scheduled-publication policy notices are no longer shown to them. Contributors and above see the dashboard as before.

  • #4128 e7414c2 Thanks @swissky! - On sites that use a route cache such as Workers Cache, creating or duplicating an entry no longer purges cached pages. The new entry is a draft that no public page shows, so pages that list the collection stay cached until a later change to published content. This applies to POST /_emdash/api/content/{collection}, POST /_emdash/api/content/{collection}/{id}/duplicate, and the content_create and content_duplicate MCP tools. content_create with status: "published" still purges the collection and the new entry.

  • #4000 20b7177 Thanks @emdashbot! - Fixes first-user passkey registration so the newly created Admin is authenticated immediately after registration verification. Clicking "Open the dashboard" now opens the dashboard instead of redirecting to the login page.

  • #4073 55adb91 Thanks @emdashbot! - Fixes force-deleting a collection so it also removes the entries' records in shared tables: revisions, SEO data, comments, content bylines, taxonomy assignments, entry locks, and the revision prune queue. Previously these rows were left behind because only the content table, FTS table, and collection row were dropped.

  • #4045 13293c5 Thanks @emdashbot! - Fixes site settings updates during a running export so the export's write_epoch bump happens inside the same transaction. If the export fence cannot record the write, the settings update now rolls back instead of committing invisibly to the export.

  • #3824 bd0095e Thanks @khoinguyenpham04! - Fixes MCP content_create and content_update rejecting writes that omit a blocks field with "must be an array". A create that omits a blocks field treats it as empty, and an update keeps the stored blocks. Supplied non-array values are still rejected.

  • #3885 02d57de Thanks @emdashbot! - Fix plugin storage cursor behavior when the anchor row of an ordered page is deleted. PluginStorageRepository.query() now encodes scalar ordered field values into the cursor and compares subsequent pages against those values, so ascending walks no longer end early and descending walks no longer restart from the top. Cursors for non-scalar values (objects or arrays) or very long strings fall back to the anchor-row lookup used before this fix, keeping those pages working while still surfacing a clear invalid-cursor error when that anchor row has been deleted. Legacy cursors that do not contain the field values still work under the same fallback.

  • #3825 52dfe3f Thanks @khoinguyenpham04! - Fixes content writes to single-type blocks fields rejecting blocks that omit _type, including nested Markdown conversion over MCP and the TypeScript client. EmDash infers the only allowed type on ordinary writes; multiple allowed types, seed files, and stored blocks of a different type still require an explicit _type. Explicit invalid types remain errors.

  • #3942 57e6507 Thanks @khoinguyenpham04! - Fixes the visual-editing rich text editor adding a revision to the entry's history every time it saves on blur. Its saves now replace the pending draft revision, like the admin editor's autosave.

  • #3982 8e1e849 Thanks @emdashbot! - Fixes new and changed redirects being ignored for pages already in the Workers Cache. Saving a redirect for an exact path now clears the cached page at that path.

  • #4102 d98e381 Thanks @emdashbot! - Fixes the MCP content_create tool so it accepts optional seo and publishedAt fields and persists them in the same call. Agents no longer need to create a post and then call content_update just to set SEO metadata or backdate an item. publishedAt is gated behind the content:publish_any permission, matching the REST API and content_update.

  • #3791 dd983cd Thanks @DiogoDuart3! - Fixes two problems with number fields in the content editor. Clearing a number field now saves it as empty instead of 0, which previously failed a field's minimum (such as min: 1) or silently stored zero. Decimal values such as 4.50 no longer block the Save button with the browser's "nearest valid values" message, while integer fields keep stepping in whole numbers.

  • #3202 24d5ea4 Thanks @eisenbruch! - Fixes full-text search so queries containing lowercase words like "and", "or", "not" or "near" are no longer parsed as FTS5 operators. This restores results for searches with possessive apostrophes and keeps prefix matching for those queries.

  • #4092 a7ff1bf Thanks @danielmlr! - Fixes WordPress imports through the EmDash Exporter plugin losing Advanced Custom Fields values: a field stored on only one post was never created, and a repeater row that only one post had was lost. For ACF field groups assigned to a post type, the import now creates a field for every ACF field and stores selects, checkboxes, repeaters, groups, relationships and other structured values as JSON. Relationship, post object, user and taxonomy values that arrive as WordPress posts, users or terms keep only their IDs, so a related post's content and password or a user's password hash are not copied into EmDash. A collection from an earlier import keeps the fields it has, so its repeaters still arrive as one field per row and sub-field.

    What should I do?

    Running the import again skips entries that already exist, so if you imported ACF relationship, post object or user fields through the exporter with an earlier version, check those JSON fields for whole WordPress posts and users yourself.

  • #988 0ce875f Thanks @masonjames! - Fixes MCP tool names for plugins with scoped IDs such as @acme/calendar: their tools are listed as acme__calendar__<tool>, which MCP clients accept. Update saved calls that use the old scoped names. Plugins with plain IDs keep their names, and the admin plugin list displays the generated names before consent.

    A plugin tool whose JSON Schema cannot be read no longer stops other tools from loading. Unreadable input schemas accept object inputs for route validation; unreadable output schemas are omitted. Duplicate generated names keep the first tool and log a warning instead of disabling the MCP endpoint.

  • #3584 2652774 Thanks @swissky! - Removes up to two database round trips from the first request after a cold start on sites with the plugin marketplace or registry enabled.

    With the sandbox disabled (sandbox: false), a failure to load the in-process plugin adapter now stops startup with an error instead of silently starting without any marketplace plugins, matching how sandboxed plugins listed in the integration config already behave.

  • #4005 ecbd1ff Thanks @saariuslystoned! - Fixes server-rendered pages that inspect public plugin route metadata before dispatching, so logged-out visitors can load plugin data. Public metadata is available through getPublicPluginRouteMeta for both visitors and signed-in users. Private route metadata stays on the signed-in getPluginRouteMeta helper.

  • #2768 9daecce Thanks @saariuslystoned! - Fixes scheduled maintenance stopping after the initialization request in Cloudflare astro dev. EmDash now drives its own maintenance through a dev-only workerd route from the long-lived dev server, keeping plugin cron jobs, scheduled publishing, cleanup, and declared plugin storage index creation running between requests without invoking application-wide scheduled jobs. Also works with HTTPS dev servers, including self-signed certificates.

  • #4053 cea0841 Thanks @danielmlr! - Fixes the defaultRole and roleMapping hover text for external auth providers and Cloudflare Access, which called role 30 "Editor" and role 40 "Developer". Role 30 is Author and can publish its own content, and role 40 is Editor. Only the descriptions and examples changed, not how roles resolve.

    What should I do?

    If you set defaultRole or a roleMapping entry to 30 because the hover text said Editor, check that Author access is what you meant. Use 20 (Contributor) for people who should not publish. The Access defaultRole still defaults to 30.

  • #3997 2ccc645 Thanks @bimsonz! - Fixes Astro's route cache, such as Workers Cache, storing a page at a redirect's source after the redirect was saved. A Worker isolate that had not yet reloaded its redirect rules could render the old page and the route cache kept it, even after the cached page was purged. Before the route cache stores a response, the redirect middleware now checks that its rules are current and answers with the redirect when they have changed. This costs one database read for each response the route cache stores; other responses are unchanged.

  • #4097 651aaf9 Thanks @emdashbot! - Fixes oversized plugin screenshots failing to load in the admin registry gallery with HTTP 413.

    The registry artifact proxy now requests the feed_fullsize image preset for screenshots instead of feed_thumbnail. This aligns the admin preview with the public plugin site and avoids the lossless upscaling that pushed modest source images over the 1 MiB proxy cap.

  • #4072 4e458bb Thanks @danielmlr! - Fixes the datetime normalization migration taking hours on large Cloudflare D1 sites and starting over after every interruption. It now writes changed rows in batches, and running emdash migrate again after a failed write continues from the last written batch.

  • #4044 e0d395d Thanks @emdashbot! - Fix saving a draft when retired fields remain only in the published content row. A full read-then-write update no longer fails validation for keys the entry already stores in its live data but that are absent from the current draft revision. Genuinely never-stored unknown keys are still rejected.

  • #3987 1dd1b37 Thanks @danielmlr! - Fixes saves in the admin and through MCP reporting an error when the configured cache provider fails to purge, although the change was already saved. These saves now report success, and the failed purge is logged.

  • #3921 2b7ad41 Thanks @emdashbot! - Fixes emdash seed to avoid leaking duplicate media rows and adds --skip-media for storage-less seeds.

    • $media references now deduplicate against existing ready media rows by content hash, so re-running emdash seed ... --on-conflict=update reuses the same rows instead of creating new ones each time.
    • New --skip-media flag resolves $media references to external URLs without downloading files or creating local ./uploads, which is useful when seeding a site whose media lives in remote object storage.
  • #4014 b60ec26 Thanks @khoinguyenpham04! - Fixes visual editing selecting all of a text field's text when it is clicked, which made the first keystroke replace the whole value. A click now places the caret where it lands, including on a field inside a link, and text selected by dragging stays selected.

  • #3942 57e6507 Thanks @khoinguyenpham04! - Fixes visual editing for empty Portable Text fields, such as the body of a new post. Edit mode now renders the inline editor for them, so the field can be written on the page instead of having no editor at all. The empty editor stays blank until it is clicked, then shows a "Type / for commands..." hint. In edit mode, an empty Portable Text field reads as an empty array instead of null.

  • #4058 7167d89 Thanks @swissky! - Warns during astro build and astro dev when the installed @emdash-cms/cloudflare version differs from emdash. The adapter depends on the exact emdash version it was released with, so upgrading only one of the two packages installs a second copy of emdash that the adapter runs against. The warning names both versions and the command to install matching ones.

  • #3265 b358838 Thanks @eisenbruch! - Fixes getEmDashCollection's where filter so an object with none of gt, gte, lt or lte (such as { in: [...] }) now logs a warning instead of silently returning unfiltered rows. Long array values no longer fail on Cloudflare D1 with "too many SQL variables": the longest arrays are each bound as one JSON parameter, keeping ordering and paging in the database on SQLite, D1 and PostgreSQL. Values bound that way compare as text.

  • #4056 3e58e0e Thanks @swissky! - Fixes WordPress WXR imports overcounting skipped items. The "skipped (already exists)" count in the admin, and the skipped value returned by POST /_emdash/api/import/wordpress/execute, now only include entries whose slug already exists in the target collection and locale. Menu items, reusable blocks (imported as sections), revisions and post types you disabled are no longer counted. The "X of Y" progress shown during the import no longer runs past the number of selected items.

  • #4113 ab47586 Thanks @emdashbot! - Send an identifying User-Agent: EmDash-Importer/1.0 header on all WordPress plugin importer requests. Without this header, hosts that block requests with an empty User-Agent (such as ScalaHosting's bot filter) returned 403 during analyze and media import. The plugin API fetcher now adds the header to every request, and direct media downloads from fetchMedia send the same value. This matches the existing media import route behavior.

  • #4150 f414d0a Thanks @swissky! - Explains why the WordPress exporter import can't connect when Cloudflare's bot protection answers with a challenge page. The URL check and the analyze and import steps now say that bot protection blocked the request and how to let it through, instead of reporting that WordPress couldn't be detected or that the analysis failed. In this case the analyze and execute endpoints return the WP_SITE_CHALLENGE error code with status 502, and probe results include challenged: true.

  • #4150 f414d0a Thanks @swissky! - Stops a WordPress exporter import with the bot-protection message when Cloudflare challenges a request for taxonomies, menus, site settings, or the media list. Previously the import skipped that part without telling you and finished without it. The analyze and execute endpoints now return WP_SITE_CHALLENGE with status 502 in these cases too.

  • #3892 5053e19 Thanks @swissky! - Fixes the WordPress export file (WXR) import in the admin keeping links to the old WordPress domain in imported content. Links to the source site are now rewritten to root-relative paths. The WordPress permalink structure is kept (for example /2024/05/my-post/), so add redirects if your routes differ. Links to uploaded files stay absolute, including in raw HTML blocks, which the WordPress plugin import previously turned into broken relative /wp-content/ links. Links inside reusable blocks imported as sections are not changed.

    The import analysis now also shows the real site title and address instead of a generic "WordPress Site" heading. For programmatic use, parseWxrString now returns the export's site metadata (it was always empty before), and content fetched through wxrSource gets the same link rewriting.

  • #4090 d93b130 Thanks @danielmlr! - Fixes WordPress XML imports creating acf_field and acf_field_group collections full of serialized field settings on sites that use Advanced Custom Fields. The admin import now skips ACF's own post types, as it already does for WordPress internals such as revisions and menu items. Collections created by earlier imports stay in place; delete them in the admin if you don't need them.

  • #4123 a065c0e Thanks @emdashbot! - Fixes media:afterUpload hooks being silently dropped for REST API multipart uploads and MCP media_create uploads on Cloudflare Workers. Both upload paths now defer hook execution through after(), matching the admin confirm flow so hooks that perform async work complete as expected.

  • Updated dependencies [6524921, f9dda4d, bda9024, bda9024, e8bd98e, 5c8ab6b, 443ca3b, ce9e405, 983c08c, ae6c1a4, d4e0af9, 718c1f2, 19fbdd6, 302e011, 3511069, 82627fd, 9d89ded, 67360d1, af92bf7, 75ef2ab, 58ac71c, 15bbfd7, e0363c3, 7a3667b, 0861e24, 3195f42, 01e1f12, dd983cd, be9378c, 0ce875f, 624390b, bf92be8, 5b227af, 83fe26d, fae844f, dd216db, 649cfae, 49319eb, dd3121c, db8325a, 7659466, d4efb97, 20ed58b, 0e8811d, f414d0a]:

    • @emdash-cms/admin@1.3.0
    • @emdash-cms/auth@1.3.0
    • @emdash-cms/gutenberg-to-portable-text@1.3.0
    • @emdash-cms/registry-client@0.7.1
    • @emdash-cms/blocks@1.3.0

Don't miss a new emdash release

NewReleases is sending notifications on new releases.