Minor Changes
-
#3062
3f516f4Thanks @swissky! - Adds agroupsetting 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
b73a133Thanks @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 wiredgetSeoMeta()by hand.Affected pages
Pages that include
<EmDashHead>and fetch their entry throughgetEmDashEntry()receive the overlay. This includes warm object-cache hits, becausegetEmDashEntry()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:metadataandpage:fragmentshooks — 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 usinggetSeoMeta(). - 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 correctsog:imageoutput everywhere the panel image is resolved: the<EmDashHead>overlay,getSeoMeta(), and image URLs in the sitemap. - The
-
#2919
b1ccecdThanks @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_LOCKEDwhere the write used to succeed. This applies to every collection once the migration has run. The response'serror.messagenames the holder anderror.detailscarries theiruserId,userName,acquiredAtandexpiresAt. Pass"overrideLock": truein the request body to write anyway, or?overrideLock=trueonDELETE, which has no body. The CLI takes--override-lockoncontent update,content delete,content publish,content unpublishandcontent 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,POSTandDELETEon/_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: falsein a seed file, or throughschema_update_collection:{ "slug": "posts", "editLocking": false }Upgrading
Includes database migration
075_entry_edit_locks. Projects on the defaultautoruntime migration mode need no action. Projects that migrate as a deployment step: runemdash migratebefore deploying this version. -
#1526
0bcb1d9Thanks @swissky! - Adds WordPress-style date tokens to collection URL patterns.url_patternnow 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
107c3ccThanks @vedanshujain! - Addsctx.storage.<collection>.updateIf(id, { where, set?, delta? })for atomic conditional updates to existing plugin documents. Usewhereto check stored fields,setto replace field values, anddeltato 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
nullcounters start at0. 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"andretryable: true, including across sandbox transports. Retry standalone calls with bounded backoff, or restart the entire explicit transaction. -
#2934
91a4aefThanks @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 realtable,tableRow,tableHeader, andtableCellnodes, so custom ProseMirror schemas that consume its output must register the existing TipTap table extensions.Pass a localized
tablePlaceholderstring toPortableTextto set the inline editor's initial table label. Omitted values retain the English label. -
#3041
0ae2f26Thanks @danielmlr! - Adds the cause to theSANDBOX_NOT_AVAILABLEerror 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 missingworker_loadersbinding orPluginBridgeexport; on Node.js it says that theworkerdbinary did not run.Sandbox runners report the cause through a new optional
unavailableReason()method onSandboxRunner. Runners without it keep the previous messages. -
#2980
570333aThanks @logelog! - AddsgetVersioned,compareAndSetandcompareAndDeleteto plugin storage collections andctx.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
nullrevision 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
36a021cThanks @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:beforeSaveandcontent:afterSavereceive an actor snapshot with the authenticated user'sidandrole. 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
userIdon content create and update entries.
-
#3026
573230fThanks @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
2b2f69eThanks @ascorbic! - Fixes Cloudflare binding failures during runtime startup returningNOT_CONFIGUREDfrom EmDash API routes. Missing D1, R2, KV, Durable Object, and Hyperdrive bindings now returnBINDING_NOT_FOUNDwith the binding-specific setup message. Invalid KV and Hyperdrive binding configuration returnsCONFIGURATION_ERROR. -
#3075
33cb7f0Thanks @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
befce6dThanks @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
cd3e391Thanks @ascorbic! - Fixes Cloudflare-backed Astro development servers hanging during initial setup or the first admin request. -
#3103
fea6bebThanks @dchaudhari7177! - Fixes the Portable TextGallerycomponent telling browsers each image fills the viewport. Itssizesattribute 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 newsizesprop toGallerywhen it renders in a container narrower than the viewport. -
#3074
3bd30daThanks @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_keyand 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
e13fa01Thanks @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
f9ac286Thanks @jakevis! - Fixes visual editing on list pages. Entries fromgetEmDashCollectionnow carry a workingeditproxy 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 fromgetEmDashEntrywere 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
4c89130Thanks @logelog! - Fixes plugin HTTP requests withallowedHostsso 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.combefore dispatch. Self-hosted deployments must permit access to that endpoint when using the default resolver. -
#2169
107c3ccThanks @vedanshujain! - Fixes a plugin storage range filter whose every bound isundefinedmatching every row instead of failing. Building a bound from an optional value —where: { stock: { gte: minStock } }whereminStockisundefined— type-checks, but contributed no SQL, soquery()andcount()returned the whole collection andupdateIf()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
StorageQueryErrornaming 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
ef22a2dThanks @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
f0af9a1Thanks @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 theLOADERbinding inwrangler.jsonc, including the named environment selected withCLOUDFLARE_ENV. Without it, config-based sandboxed plugins do not load and marketplace or registry installs returnSANDBOX_NOT_AVAILABLE, while browsing remains available. -
#3076
dd5ef1aThanks @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
27e432eThanks @ascorbic! - Fixes invalid plugin registry settings causing the admin manifest to fail with a generic server error. EmDash reports malformedexperimental.registryfields while Astro loads the site configuration. If invalid registry settings reach the runtime, the admin remains available and shows which field to correct inastro.config.mjs. -
#2043
d409722Thanks @swissky! - Taxonomy term counting no longer sends queries against declared collections that were never created, eliminating the phantomno such table: ec_postsdatabase error logs that sites without apostscollection 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
8b3fd50Thanks @danielmlr! - Fixes taxonomy and menu translations being accepted under a different name than their source. Creating a taxonomy or a menu withtranslationOfthrough the REST API or the MCPtaxonomy_createandmenu_createtools now returns a validation error unlessnamematches the source. -
#3092
1a71c9eThanks @emdashbot! - Fix stale revision tokens after unpublish, discard, and revision restore.The admin editor now reads the new
_revreturned 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
91a4aefThanks @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