github emdash-cms/emdash emdash@0.38.0

latest releases: @emdash-cms/plugin-test@0.1.0, @emdash-cms/sandbox-workerd@0.6.0, @emdash-cms/plugin-embeds@0.1.46...
4 hours ago

Minor Changes

  • #3062 3f516f4 Thanks @swissky! - Adds a group setting to collections. Collections that share a group render as one collapsible folder in the admin sidebar, positioned where the first of them appears; a taxonomy joins the folder when every collection it is assigned to is shown in that folder. A folder you have not touched opens while one of its members is active; once you open or close it yourself, the sidebar remembers that choice in the browser. Set the group in the content type editor under Navigation, in seed files, or through the schema API and the MCP collection tools; leaving it empty keeps today's flat list.

  • #1963 b73a133 Thanks @swissky! - <EmDashHead> now applies the entry's SEO panel values (title, description, image, canonical, noindex) automatically on server-rendered content pages. Previously the panel was silently ignored unless the page wired getSeoMeta() by hand.

    Affected pages

    Pages that include <EmDashHead> and fetch their entry through getEmDashEntry() receive the overlay. This includes warm object-cache hits, because getEmDashEntry() primes the same request-scoped cache from the cached snapshot when the loader never runs. Multi-entry collection results (e.g. getEmDashCollection()) are not currently covered.

    What editors can override

    Editor-set panel values replace the template-provided base fields for description, og:title, og:description, og:image, the canonical URL, and robots. They also feed the JSON-LD structured data, so head tags and structured data stay in sync.

    What plugins see

    Plugin page:metadata and page:fragments hooks — in the head and in the body components — receive the overlaid page context, but plugin contributions still win via first-wins dedup.

    What does not change

    • The <title> element remains the template's responsibility.
    • Prerendered pages and pages that bypass <EmDashHead> keep using getSeoMeta().
    • No additional database query is made; the panel data rides along on the entry query the page already runs.

    Canonical and image URL resolution

    getSeoMeta() now resolves an explicit SEO panel canonical through the same resolver as <EmDashHead>: root-relative values (/custom-path) are absolutized against the site URL when one is configured (previously they were returned unchanged), and protocol-relative values (//host/path) pass through untouched. The same panel value now produces the same canonical URL on both paths.

    Protocol-relative SEO image references (//cdn.example.com/x.png) are no longer prefixed with the site URL, which previously produced a broken doubled-path URL. This corrects og:image output everywhere the panel image is resolved: the <EmDashHead> overlay, getSeoMeta(), and image URLs in the sitemap.

  • #2919 b1ccecd Thanks @danielmlr! - Adds an edit lock per content entry, so two people no longer discover a collision only after both have done the work.

    Opening an entry in the admin takes a lock on it. A second editor is told who has it and chooses between opening the entry read-only, where nothing they type can be lost to a refused save, and taking it over. After a take-over, the previous holder is told within two minutes that the entry moved on, their next save is refused, and a banner names who holds it now.

    The lock lasts seven minutes. The admin renews it every two minutes while the entry is open, so a pause in typing does not lose it, and every save on the entry extends it too. Leaving the editor or closing the tab releases it, as does moving the entry to the trash; a tab that loses power or network lets it lapse.

    Who is newly refused

    Scripts, API tokens and the CLI that update, delete, publish, unpublish, schedule or discard an entry while an editor has it open in the admin now receive 409 ENTRY_LOCKED where the write used to succeed. This applies to every collection once the migration has run. The response's error.message names the holder and error.details carries their userId, userName, acquiredAt and expiresAt. Pass "overrideLock": true in the request body to write anyway, or ?overrideLock=true on DELETE, which has no body. The CLI takes --override-lock on content update, content delete, content publish, content unpublish and content schedule. The MCP content tools do not honour the lock yet.

    Locks are per entry and per locale, so two translations of the same entry can be edited at once.

    Take or read a lock directly through GET, POST and DELETE on /_emdash/api/content/{collection}/{id}/lock.

    Turning it off

    Locking is on for every collection. Switch it off under Content Types → your collection → Edit locking, with editLocking: false in a seed file, or through schema_update_collection:

    { "slug": "posts", "editLocking": false }

    Upgrading

    Includes database migration 075_entry_edit_locks. Projects on the default auto runtime migration mode need no action. Projects that migrate as a deployment step: run emdash migrate before deploying this version.

  • #1526 0bcb1d9 Thanks @swissky! - Adds WordPress-style date tokens to collection URL patterns. url_pattern now supports {year}, {month}, {day}, {hour}, {minute}, {second} (resolved from the entry's publish date, zero-padded) alongside {slug} and {id} — so you can reproduce permalinks like /{year}/{month}/{day}/{slug}.html. The tokens resolve everywhere the pattern is used: sitemap canonical URLs, hreflang alternates, navigation menu links, slug-change auto-redirects, and the admin's preview and "View published" links. Tokens stay literal when an entry has no publish date, so canonical URLs remain stable across edits.

  • #2169 107c3cc Thanks @vedanshujain! - Adds ctx.storage.<collection>.updateIf(id, { where, set?, delta? }) for atomic conditional updates to existing plugin documents. Use where to check stored fields, set to replace field values, and delta to increment or decrement integer counters. The method returns { applied: true, data } with the updated document, or { applied: false } when the document is absent or the condition fails. It never inserts a document.

    Malformed update arguments reject without writing. Deltas require safe integer operands and results; missing or null counters start at 0. Invalid stored counters, overflow, and non-object documents return { applied: false } without changing any fields.

    Available to native plugins and sandboxed plugins on Cloudflare and Workerd, with SQLite, D1, and PostgreSQL support. PostgreSQL serialization failures and deadlocks expose code: "STORAGE_SERIALIZATION_FAILURE" and retryable: true, including across sandbox transports. Retry standalone calls with bounded backoff, or restart the entire explicit transaction.

  • #2934 91a4aef Thanks @khoinguyenpham04! - Adds responsive, lossless Portable Text tables with an accessible size picker, complete row and column controls, merge and split, persistent column widths, HTML and spreadsheet clipboard support, keyboard navigation, and right-to-left resizing. Wide tables keep their horizontal position while resizing, hide native scrollbar chrome, and show edge shadows for hidden columns.

    Use the compact, scrollable Table menu for structural actions, or press Backspace or Delete to remove selected full rows or columns. Undo restores the removed content and structure.

    The editor toolbar no longer includes Spotlight Mode, leaving more room for table controls at the standard editor width.

    The public renderer now preserves table headers, spans, alignment, and preferred widths. Existing legacy string-cell tables continue to render. portableTextToProsemirror() now returns real table, tableRow, tableHeader, and tableCell nodes, so custom ProseMirror schemas that consume its output must register the existing TipTap table extensions.

    Pass a localized tablePlaceholder string to PortableText to set the inline editor's initial table label. Omitted values retain the English label.

  • #3041 0ae2f26 Thanks @danielmlr! - Adds the cause to the SANDBOX_NOT_AVAILABLE error and to the "Plugin sandbox is configured but not available on this platform" startup warning when a configured sandbox runner cannot run plugins. On Cloudflare Workers the message names the missing worker_loaders binding or PluginBridge export; on Node.js it says that the workerd binary did not run.

    Sandbox runners report the cause through a new optional unavailableReason() method on SandboxRunner. Runners without it keep the previous messages.

  • #2980 570333a Thanks @logelog! - Adds getVersioned, compareAndSet and compareAndDelete to plugin storage collections and ctx.kv. Native and sandboxed plugins can create an absent key or condition a replacement or deletion on the revision they read, preventing concurrent requests from silently overwriting each other.

    Pass an explicit null revision to create only when absent. A successful replacement returns its new revision; a conflict returns { applied: false }. Invalid input, permission failures and database failures reject the promise. Atomicity applies to one key, so changes spanning multiple records still require an application-level protocol.

    Update core and the sandbox adapter together and apply the host database migrations before using the methods. The migration initializes existing records without a backfill. Stored values are preserved, and existing unconditional writes continue to work while invalidating old revisions. Conditional keys are limited to 1,024 JavaScript string characters and values to 1 MiB of UTF-8 JSON.

Patch Changes

  • #3057 36a021c Thanks @emdashbot! - Fixes content attribution for authenticated REST, visual editing, and MCP saves.

    • Revisions record the acting user without changing the entry owner. MCP updates preserve the existing owner, and actorless internal writes leave revision attribution unset instead of inferring it from ownership.
    • content:beforeSave and content:afterSave receive an actor snapshot with the authenticated user's id and role. The snapshot is isolated between hooks so one plugin cannot change the attribution seen by another.
    • The audit-log plugin stores the actor ID as userId on content create and update entries.
  • #3026 573230f Thanks @ascorbic! - Fixes snapshot exports and content backups on PostgreSQL so preview snapshots, manual backups, and scheduled backups include the same content and portable schema metadata as SQLite.

  • #3065 2b2f69e Thanks @ascorbic! - Fixes Cloudflare binding failures during runtime startup returning NOT_CONFIGURED from EmDash API routes. Missing D1, R2, KV, Durable Object, and Hyperdrive bindings now return BINDING_NOT_FOUND with the binding-specific setup message. Invalid KV and Hyperdrive binding configuration returns CONFIGURATION_ERROR.

  • #3075 33cb7f0 Thanks @ascorbic! - Fixes overlapping marketplace or registry plugin updates and downgrades deleting the active plugin bundle. Updates retain previous version bundles so delayed work cannot remove a version that becomes active again.

  • #3078 befce6d Thanks @ascorbic! - Adds a fail-closed first-release exemption to the plugin registry's optional minimum release age policy. A package's first release can install immediately only when the aggregator reports exactly one retained release and confirms that it continuously observed the package's release history.

    Existing packages, backfilled packages, and packages with missing or incomplete history remain subject to the configured holdback. Deleted releases still count, and explicit publisher or package exemptions continue to work.

  • #3105 cd3e391 Thanks @ascorbic! - Fixes Cloudflare-backed Astro development servers hanging during initial setup or the first admin request.

  • #3103 fea6beb Thanks @dchaudhari7177! - Fixes the Portable Text Gallery component telling browsers each image fills the viewport. Its sizes attribute now describes one grid cell, using the gallery's column count and its two-column layout at 640px and below, so browsers stop downloading oversized images for multi-column galleries. Pass the new sizes prop to Gallery when it renders in a container narrower than the viewport.

  • #3074 3bd30da Thanks @eisenbruch! - Fixes visual editing saving a new draft when nothing changed. With edit mode on, the inline Portable Text editor saved the body every time focus left it and every time the page was left, even if no one had typed, because each save check compared freshly generated block keys against the stored ones. Entries picked up drafts that differed only in _key and showed "Pending changes" in the admin. The editor now compares its document with the one last saved, so an unedited body is never saved and a real edit is saved once.

  • #3090 e13fa01 Thanks @danielmlr! - Fixes non-translatable fields never reaching an entry's other translations on collections with revisions, which is the default. Publishing an entry now copies the non-translatable values it changed to the rest of its translation group, and a save that started from a translation's earlier version gets a conflict instead of overwriting the copied values. Values that already differ between translations stay as they are until a change to that field is published in one of them.

  • #2970 f9ac286 Thanks @jakevis! - Fixes visual editing on list pages. Entries from getEmDashCollection now carry a working edit proxy in edit mode, so spreading {...entry.edit.title} renders the annotation and the toolbar makes the element editable. Previously every collection entry received a no-op proxy in every mode, so only pages built from getEmDashEntry were click-to-edit — fields shown exclusively in a list, and collections with no detail page, could not be edited on the page at all.

  • #3050 4c89130 Thanks @logelog! - Fixes plugin HTTP requests with allowedHosts so initial URLs and redirects also pass SSRF validation. Requests are rejected when URL or DNS validation identifies an unsupported scheme or a non-public address.

    Existing callers of the shared outbound URL validator also reject these non-public ranges.

    The default validator resolves public hostnames through cloudflare-dns.com before dispatch. Self-hosted deployments must permit access to that endpoint when using the default resolver.

  • #2169 107c3cc Thanks @vedanshujain! - Fixes a plugin storage range filter whose every bound is undefined matching every row instead of failing. Building a bound from an optional value — where: { stock: { gte: minStock } } where minStock is undefined — type-checks, but contributed no SQL, so query() and count() returned the whole collection and updateIf() applied its write with no guard at all. A guarded decrement could then drive a counter past the bound the caller asked for.

    Such a filter now throws StorageQueryError naming the field. Pass a defined bound, or omit the field when you mean to match unconditionally:

    const where = minStock === undefined ? {} : { stock: { gte: minStock } };
    await ctx.storage.products.query({ where });
  • #3091 ef22a2d Thanks @danielmlr! - Fixes permanently deleting an entry leaving its taxonomy term assignments in the database. The assignments are removed with the last translation of the entry and kept while another translation exists, including one in the trash.

  • #2351 f0af9a1 Thanks @MattieTK! - New Cloudflare projects leave the paid-plan Worker Loader binding disabled so they can deploy on the Workers free plan. Enable sandboxed plugins in the scaffold prompt or with --sandboxed-plugins.

    The Cloudflare sandbox() helper now selects the runner from the LOADER binding in wrangler.jsonc, including the named environment selected with CLOUDFLARE_ENV. Without it, config-based sandboxed plugins do not load and marketplace or registry installs return SANDBOX_NOT_AVAILABLE, while browsing remains available.

  • #3076 dd5ef1a Thanks @ascorbic! - Fixes marketplace plugin updates so administrators review newly requested capabilities, public routes, and MCP tools before granting them. Update confirmation remains pinned to the version that was reviewed, so a newer release requires a separate review.

  • #3077 27e432e Thanks @ascorbic! - Fixes invalid plugin registry settings causing the admin manifest to fail with a generic server error. EmDash reports malformed experimental.registry fields while Astro loads the site configuration. If invalid registry settings reach the runtime, the admin remains available and shows which field to correct in astro.config.mjs.

  • #2043 d409722 Thanks @swissky! - Taxonomy term counting no longer sends queries against declared collections that were never created, eliminating the phantom no such table: ec_posts database error logs that sites without a posts collection produced on every uncached taxonomy render. On multi-isolate deployments, term counts now pick up a collection created or deleted on another isolate within about a minute instead of immediately; the write-handling isolate reflects it at once.

  • #3101 8b3fd50 Thanks @danielmlr! - Fixes taxonomy and menu translations being accepted under a different name than their source. Creating a taxonomy or a menu with translationOf through the REST API or the MCP taxonomy_create and menu_create tools now returns a validation error unless name matches the source.

  • #3092 1a71c9e Thanks @emdashbot! - Fix stale revision tokens after unpublish, discard, and revision restore.

    The admin editor now reads the new _rev returned by unpublish, discard-draft, and revision-restore responses and advances its optimistic-concurrency token before the next save. Unpublish also flushes pending editor changes before sending the request, matching the ordering already used for publish, schedule, and publication-date changes, and it now catches the promise rejection when the action is blocked by invalid fields or a click while another publishing action is already in progress. This prevents subsequent autosaves or publish actions from being refused as a 409 conflict, stops unpublished posts from overwriting unsaved edits, and avoids unhandled promise rejections from the unpublish button.

  • #2934 91a4aef Thanks @khoinguyenpham04! - Fixes entries in wide collections failing to load on Cloudflare D1 with a "too many columns in result set" error.

  • Updated dependencies [da171b3, 7bbd8ea, 5f51e55, befce6d, a350627, 3f516f4, 9d5d8ed, b1ccecd, d2ad846, d30207d, 4cc150e, 0bcb1d9, 91a4aef, dd5ef1a, 64344d3, cba1135, 27e432e, 1a71c9e, 0a723e9]:

    • @emdash-cms/registry-client@0.6.0
    • @emdash-cms/admin@0.38.0
    • @emdash-cms/registry-lexicons@0.5.0
    • @emdash-cms/registry-verification@0.3.1
    • @emdash-cms/auth@0.38.0
    • @emdash-cms/gutenberg-to-portable-text@0.38.0

Don't miss a new emdash release

NewReleases is sending notifications on new releases.