github emdash-cms/emdash emdash@1.4.0

Minor Changes

  • #3856 064fbbf Thanks @swissky! - Adds admin.hiddenNavItems to hide built-in admin areas a site doesn't use. Set it to any of calendar, media, comments, menus, redirects, widgets, sections, bylines, and import, for example admin: { hiddenNavItems: ["comments", "redirects"] }. Hidden areas disappear from the sidebar and the command palette. On the dashboard, listing media hides the Upload Media button, and listing calendar keeps the Scheduled count but removes its link to the calendar. The pages and API routes stay reachable for users with the required role. An unknown name fails the build.

Patch Changes

  • #4364 75030fc Thanks @danielmlr! - Fixes translating a menu on Cloudflare D1, which failed for larger menus and left an empty translated menu behind. On D1, translating or deleting a menu and reordering or replacing its items now either save completely or leave the menus unchanged.

    What should I do?

    If translating a menu failed before, delete the empty translated menu it left behind, then translate the menu again.

  • #4167 ec973c0 Thanks @cyface! - Fixes extra database round trips on getEmDashEntry and getEmDashCollection for sites that register byline custom fields or have entries with an author but no explicit byline credit. These reads now resolve bylines from the content query itself and load custom-field values in one batched step, instead of re-reading every entry's credits. On Cloudflare D1, a page that reads a few entries saves two to six sequential queries.

  • #4287 ceaf9b6 Thanks @danielmlr! - Fixes expired OAuth tokens and unused login codes from emdash login and MCP clients staying in the database forever. Scheduled cleanup now deletes them after they expire.

  • #4286 3d29980 Thanks @danielmlr! - Fixes lost edits on D1, Durable Objects and PostgreSQL when two saves of the same entry, both based on the same version, arrive at once, for example from two MCP clients or from scripts using emdash content update --rev: both reported success, and the first was silently overwritten. The second save now returns 409 CONFLICT, the same response a save based on an outdated version already gets.

  • #4459 9d9fd82 Thanks @emdash-autofix! - Fixes POST /_emdash/api/oauth/token returning uppercase INVALID_GRANT for refresh-token and device-code failures. These responses now use the RFC-standard invalid_grant code so OAuth clients can recognize rejected grants and restart authorization.

  • #4243 3ce9d36 Thanks @emdashbot! - Fixes Cloudflare-cached pages remaining outdated after content is deleted or unpublished. Pages using EmDash cache hints no longer send an automatic Last-Modified header, so Cloudflare stops answering 304 Not Modified based on an entry timestamp that does not cover related lists or settings changes. This applies to entry, collection, publication-date, and reference-list hints. Sites using an object cache reload those content snapshots once after upgrading so cached validators from earlier versions are not reused.

    Taxonomy definitions and collection-to-taxonomy mappings also revalidate in the background after 30 seconds, so long-lived server processes pick up changes saved elsewhere without adding a blocking query to each render. A failed refresh retains the last good snapshot.

    What should I do?

    cacheHint.lastModified is no longer populated automatically. For an entry's displayed modification date, use entry.data.updatedAt instead; it is not a validator for the whole page.

    Pages relying on automatic validators also lose Astro's implicit Cache-Control: no-cache header. Set your own browser Cache-Control if you need a fresh check on every visit. If you want conditional requests, supply a custom lastModified hint to Astro.cache.set(). Its timestamp must advance whenever any part of the response changes, including settings, related entries, deletions, and unpublishes.

  • #4157 8da0792 Thanks @opariffazman! - Fixes external auth providers, such as Cloudflare Access, writing the session store on every authenticated /_emdash request. EmDash now writes the session only when the signed-in user differs from the stored one, so sites on Cloudflare no longer spend one Workers KV write per request, which could exhaust the Workers Free plan's daily write limit and hit KV's one-write-per-second limit on a key for parallel requests.

  • #3192 c238da2 Thanks @bazemoreb! - Names media downloads after the file that was uploaded. The public media file route previously sent Content-Disposition: attachment with no filename, so browsers fell back to the last path segment of the URL and saved every PDF, DOCX or CSV under its storage key. The route now looks the original filename up by storage key and emits both the quoted ASCII filename and the RFC 5987 filename* form, so non-ASCII names survive.

    Media with no matching database row is served exactly as before, and a failed lookup never fails the download.

  • #3129 a6f4091 Thanks @eisenbruch! - Fixes saving an entry turning an empty image or file field ("") into { "provider": "external", "id": "", "src": "" }. A blank image or file field, or a blank image inside a repeater, is now saved as null, the canonical empty value. Entries with no image stop changing on every save after that.

    A required image or file field now returns a validation error for "" instead of saving an empty image. This only affects API, MCP or import clients, because the admin never sends "".

  • #4296 f197acf Thanks @danielmlr! - Fixes media usage tracking in collections that have a reference field named title or name, where saving an entry recorded none of the images and files it uses. Saving now records them.

    What should I do?

    Entries in those collections that were saved before this release can still be missing from Used in after you upgrade. Open Settings → Media usage tracking and wait until it shows Ready. If it shows Needs attention instead, repair each affected collection once:

    npx emdash media repair-usage --collection <collection>
  • #4285 57abb5c Thanks @danielmlr! - Fixes menus showing dead links to entries that are in the trash or not published. getMenu() leaves those items out. Any item getMenu() leaves out now takes its child items with it, instead of moving them to the top level of the menu. In edit mode, menus still link to unpublished entries. With the object cache enabled, a cached menu now refreshes when an entry or taxonomy term it links to changes.

  • #4365 e1c4a87 Thanks @danielmlr! - Fixes creating a child menu item through the menu items API on PostgreSQL. A request with a parentId and no sortOrder failed; the item is now placed after its existing siblings.

  • #4208 bed3e42 Thanks @danielmlr! - Fixes content list search, in the admin and through the content list API's q parameter, failing when the collection has a reference field named title or name. The search now leaves out a reference field bound to a relation and matches the entry's other fields.

  • #4205 4409acb Thanks @danielmlr! - Fixes entry creation, entry saves and content-list search failing with no such column in a collection that has a reference field marked searchable. Adding such a field to a search-enabled collection failed, and after a seed turned search on, creating or saving any entry in that collection failed. Site search and content-list search now ignore the searchable flag on a reference field bound to a relation.

    What should I do?

    If creating or saving entries in a collection still fails after upgrading and the server log shows no such column, open the collection under Content Types, untick Search and save, then tick it again and save. This rebuilds the collection's search index.

  • #4386 53acf71 Thanks @Stormster! - Fixes entry slugs being generated from a field named title instead of the collection's configured title field. In a collection whose titleField is name and which also has a title field (a person's job title, say), typing in the editor now fills the slug from the name, and entries created through the API without a slug get one from the name too, so two people with the same job title no longer collide with SLUG_CONFLICT. Collections without a titleField keep the previous title, then name, order.

  • #4394 6be7263 Thanks @emdash-autofix! - Fixes sandboxed plugin ctx.taxonomies mutations (createTerm(), addEntryTerms(), and removeEntryTerms()), ctx.content.create(), versioned content actions (getVersioned(), publish(), unpublish(), schedule(), unschedule(), getTrashedVersioned(), and restore()), and ctx.comments.setStatus() on Cloudflare when requests run across multiple Worker isolates. Content creation consistently uses runtime validation and hooks and preserves translationOf links instead of falling back to a plain database insert.

    Runner-created invocations without a content-create callback now receive Content creation is not configured rather than a database insert that bypasses runtime behavior. createPluginTestHost() supplies a database-backed callback with content-handler validation so transport-level tests can continue creating content and linked translations without starting the full EmDash runtime. It preserves the transport host's support for data.slug and data.status.

    Await these operation promises before returning from a plugin route or hook. Their RPC connection now lasts only for that invocation; unlike the previous loopback binding, it cannot support work that outlives the invocation.

    Publication after-hooks remain deferred. Same-entry actions attempted while an after-hook is running can return CONTENT_ACTION_REENTRANT; faster callback routing can expose this overlap in tests that assumed hooks had already finished. In runtime-host tests, call await host.waitForDeferredTasks() after an action before inspecting its after-hook effects or starting another action on the same entry. This method waits for queued deferred runtime work, including work those tasks enqueue; it does not run future scheduled tasks or wait for arbitrary unawaited plugin operations.

  • #4380 9e7cd3d Thanks @emdash-autofix! - Fixes emdash site import failing with COLLECTION_EXISTS when manually deleted target collections still have media-usage cleanup pending. For these manually deleted collections, import now runs as many cleanup steps as the request budget allows, then imports the collection in the same request if enough budget remains. It waits for active deletion leases or retry delays before reusing a slug. Cancellation during this per-collection cleanup stops before importing the collection. Failed cleanup of a manually deleted collection includes recovery instructions, and collection schema error messages are preserved in status errorDetail.

  • #4276 a16ab2c Thanks @emdash-autofix! - Fixes site builds using zustand/traditional or other dependencies that default-import use-sync-external-store shims, which previously failed with a missing default export error.

  • #4396 9201138 Thanks @emdash-autofix! - Fixes keyboard navigation and entrance animations in the admin and visual editor's floating toolbars with Tiptap 3.20.3 and later. Escape closes image alt-text editing, Shift+Tab returns to the caption, and arrow keys move between toolbar controls again.

  • #3279 d1e25e0 Thanks @eisenbruch! - Fixes the visual editing toolbar's "Open in admin" so each entry gets its own admin window. The window name was a single constant shared by the whole site, so opening the admin for one entry navigated away from another already open in that tab. A repeat click on the same entry still reuses that entry's window.

  • #3596 6c5d878 Thanks @swissky! - Caches widget areas in the object cache. With an object cache configured, <WidgetArea>, getWidgetArea(), getWidgetAreaWithCacheHint() and getWidgetAreas() no longer query the database on each page load. Widget changes made in the admin, by seeding or by a site transfer invalidate the cached areas; code that writes the widget tables directly should call the new invalidateWidgetObjectCache() export.

  • #3890 56cb9da Thanks @swissky! - Fixes importing WordPress custom post types with a hyphen in their name (such as team-member) through the EmDash Exporter plugin. The import created the team_member collection but then failed every entry with Collection "team-member" does not exist, and custom taxonomies for that post type were not linked to the collection. The review step now shows the collection the import will use and recognizes it on a repeated import, so field type conflicts with it are reported. The wxrSource import source now suggests the same valid collection names, such as team_member for team-member and wp_content for a post type named content.

  • Updated dependencies [6cc69bb, 3fcf437, 064fbbf, f0956f1, 500a45b, 2a62e8a, 5cd334e, b82016d, 0bc006c, d840b08, a1a0174, a6b28a6, e80c1f0, 936a4cb, 936a4cb, 936a4cb, e80c1f0, 190022c, 9e720d6, 2ed2755, 229c64b, d60c43c, dd0bc9e, 4e4b068, 0bc23ad, 7a04c81, 046ec3c, 15557d1, 2e103a4, 245a562, fa46578, 585cd22, e847d4b, e6ee72f, 53acf71, 0709277, 6094ff8, d160cb0, 20a5923, 9201138, 26c4faf, 3ad08dc]:

    • @emdash-cms/admin@1.4.0
    • @emdash-cms/blocks@1.4.0
    • @emdash-cms/auth@1.4.0
    • @emdash-cms/gutenberg-to-portable-text@1.4.0

Don't miss a new emdash release

NewReleases is sending notifications on new releases.