emdash@0.37.0
Minor Changes
-
#2897
f622a17Thanks @ascorbic! - Addsidto thecontent:beforeSavehook event when an existing item is updated, for both trusted and sandboxed plugins.event.contentholds only the submitted field values, so a hook that needs the stored item, for example to audit or validate a change against it, can callctx.content.get(event.collection, event.id). The field is absent on creates. -
#2899
595a6b1Thanks @khoinguyenpham04! - Adds Replace image to the Media Library for ready JPEG, PNG, and WebP files stored by EmDash.Choose a same-format file to update every existing use of an image while preserving its media ID, filename, URL, alt text, caption, and location. The replacement can use different dimensions or an aspect ratio from the original. EmDash overwrites the original bytes and clears the focal point; it does not retain the previous file. The action works with local disk, R2, and S3-compatible storage.
-
#2861
05d5596Thanks @khoinguyenpham04! - Adds cropping for JPEG, PNG, and WebP images stored by EmDash on local disk, Cloudflare R2, or S3-compatible storage.Move and resize a rule-of-thirds crop frame with corner handles for fixed ratios and eight handles for Freeform. Choose the original ratio, Freeform, or a common aspect ratio. Create cropped copy creates a separate media item with any ratio and names it for the selected ratio or output dimensions. Replace original uses the original ratio and replaces the existing item under the same ID and URL, so every reference uses the cropped image without rewriting or republishing content. Local media and responsive renditions revalidate their stable URLs so sites load the replacement instead of keeping a stale cached image. The original bytes and crop history are not retained.
-
#2912
6da29d3Thanks @danielmlr! - Breaking (MCP clients): Requires_revon the MCPcontent_update,content_publish,content_unpublishandcontent_discard_drafttools, so an agent can no longer write over changes it never read. The CLI has always required the token oncontent update; the MCP surface now matches it.The four tools previously accepted
_revas an optional parameter and performed the write when it was omitted. Such a call now fails validation with a message namingcontent_get. To migrate, read the item first and pass back the token from the response:{ "collection": "posts", "id": "01K4EXAMPLEID0000000000", "data": { "title": "New title" }, "_rev": "MzoyMDI2LTA5LTA0IDEyOjMwOjAw" }The token is opaque; pass it through unchanged. A write built on a stale token fails with
CONFLICT, so read the item again and retry with the new token. There is no option to restore the previous behavior. The tool descriptions state the same protocol, so an agent reading the schema follows it without being told. -
#2926
556c9feThanks @emdashbot! - Adds MCP tools for managing taxonomy definitions:taxonomy_get,taxonomy_create,taxonomy_update, andtaxonomy_delete.These mirror the REST endpoints added in #2431, so MCP clients can now create taxonomies before adding terms instead of dropping out to a hand-rolled API call.
taxonomy_createacceptsname,label,labelSingular,hierarchical,collections,locale, andtranslationOf. WhentranslationOfis used andhierarchicalorcollectionsare omitted, the new definition inherits them from the source taxonomy, fixing the defaulting trap described in #2525. -
#2746
c7b6fdfThanks @ascorbic! - AddsDirectPdsClientfor reading package profiles and releases with AT Protocol repository proofs, and updates experimental decentralized registry installs and updates to verify current signed records directly from the publisher's PDS.Aggregator record integrity
Install and update reject aggregator-supplied profile or release metadata whose URI or CID does not match the publisher's signed records. The server returns
AGGREGATOR_RECORD_MISMATCHbefore fetching the artifact or requesting consent.Publisher identity display
The admin treats handle resolution as an advisory identity signal. It keeps the install button disabled while attempting to resolve the package DID back to a handle, then blocks installation when
resolveDidToHandle()conclusively returns"invalid". An indeterminate result caused by a network failure, unsupported DID method, or missing handle displays the publisher DID and does not block installation.Install and update trust the publisher DID and the signed repository proofs for the profile and release records. A handle is display metadata and is not an authorization or record-integrity input.
Provenance and release policy
The installer applies the signed profile's release policy, independently fetches and verifies supplied Sigstore/SLSA provenance, and binds moderation labels to the exact profile or release CID. Missing required provenance and any supplied provenance that is unavailable, malformed, mismatched, or unsupported block installation and updates. Artifact checksums, archive paths, bundle limits, manifest identity, and version use the same verification rules as the registry release tooling.
The verification package also exports
inspectPackageReleaseRecordsfor validating signed records and policy before artifact and provenance evidence is available.Registry install and update consent now show the exact verified profile and release CIDs, signed publisher policy, and provenance status. Install consent uses permissions and MCP tools read from the verified bundle rather than the aggregator's record copy.
Install, update, and delegated-release verification require lowercase base32 multibase
sha2-256multihashes for package artifacts and provenance documents. The plugin CLI already produces this format. The authenticated image-artifact proxy still accepts legacy bare hexadecimal SHA-256 checksums for display-only images.
Patch Changes
-
#2895
76946e4Thanks @ismail-rt! - Fixes admin “View published” and “Live View” links so translated entries include the locale prefix required by the site’s Astro i18n routing configuration. -
#2876
ad19827Thanks @ismail-rt! - Fixes the Archives widget so monthly and yearly lists include posts whosepublishedAtvalue is aDateobject instead of rendering an empty list. -
#2783
cd294dcThanks @yumam0815! - Fixes publication workflows so callers can pass the approved_revto publish, unpublish, or discard a draft and receive aCONFLICTresponse when the entry changed. -
#2852
b06fc63Thanks @MA2153! - Fixes scheduled media-usage cleanup reading far more rows than its batch size on large sites. A cleanup run that had lost its lease scanned the whole occurrence table before returning nothing, so cron ticks could spike into the hundreds of thousands of rows read. Sites on Cloudflare D1 will see those spikes disappear. -
#2864
ecdba4dThanks @camc314! - Updates Zod to 4.5 while keeping EmDash and native plugin schemas on one compatible version. Existing minute-precision ISO datetimes remain valid, and URL content fields continue to enforce configured length and pattern rules. -
#2885
7a5d9c1Thanks @MA2153! - Fixes byline profile pages having no way to render the byline's avatar (#2613).getByline,getBylineBySlug, and the underlying single-rowBylineRepositoryfinders now resolve the avatar's media row in the same query, soavatarStorageKey,avatarAlt,avatarBlurhash, andavatarDominantColorare populated alongsideavatarMediaId:--- import { getBylineBySlug } from "emdash"; const byline = await getBylineBySlug(Astro.params.slug, { locale: Astro.currentLocale, }); const avatar = byline?.avatarStorageKey ? Astro.locals.emdash.getPublicMediaUrl(byline.avatarStorageKey) : null; --- {avatar && <img src={avatar} alt={byline.avatarAlt ?? byline.displayName} />}
Previously these fields were populated only when a byline was hydrated as a credit on a content entry, so a page keyed on the byline itself —
/authors/<slug>and the like — held a bare media id with no public API to turn it into a URL. Nothing else changes: the lookup still costs one query (the avatar is aLEFT JOIN, not a second round trip), andfindManystill skips the join, so byline list pages are unaffected. -
#2886
de122b4Thanks @ascorbic! - Fixes unpublishing content resetting its publication date. Previously published drafts keep their date visible and editable in the admin, and republishing them without a date override reuses it. -
#2931
6676283Thanks @khoinguyenpham04! - Fixes the editor image settings panel overflowing at narrow widths and aligns its fields, help, and actions with the standard editor sidebar.Changing image alignment or text preserves the existing display size. Reset clears custom dimensions, constrained editor images retain their aspect ratio, floated images stay visible, and None and Center have distinct positions.
Preserves image alignment through the exported Portable Text converters. Image settings offer None, Left, Center, and Right; existing imported Wide and Full values and public theme hooks are retained.
-
#2945
d418b64Thanks @emdashbot! - Chrome subsystems (site settings, menus, taxonomies and widget areas) now invalidate the Workers edge cache when mutated through the admin API, and public read helpers gained additive*WithCacheHintvariants that return page-level cache hints. Stable invalidation tags areemdash:settings,emdash:menu:<name>,emdash:taxonomy:<name>andemdash:widget-area:<name>. -
#2822
60691dfThanks @jcheese1! - FixesgetEmDashCollection()andgetEmDashEntry()returning SQLite-backed boolean fields as0or1. Boolean fields now returntrueorfalse, matching their generated TypeScript types, while integer fields retain numeric values. -
#2936
062e8beThanks @emdashbot! - Regenerateemdash-env.d.tsimmediately when the schema changes duringastro dev, so newly added fields and collections are available in types without reloading the dev server. -
#2925
b44bc2cThanks @emdashbot! - Fixes the OpenAPI document for content taxonomy terms so it matches the shipped route. The documentedPUT /_emdash/api/content/{collection}/{id}/termspath has been removed; useGETorPOST /_emdash/api/content/{collection}/{id}/terms/{taxonomy}instead. Thetaxonomypath parameter is required, andPOSTvalidates that every term id belongs to that taxonomy. -
#2900
9def325Thanks @khoinguyenpham04! - Fixes Cloudflare development servers failing during cold start with Astro 7.3.1 after Vite discoversastro/logger/consoleand invalidates prebundled server chunks. -
#2921
67f676dThanks @emdashbot! - Fixes the inline Portable Text editor failing to hydrate in development when visual editing is enabled. The editor's code-block extension loads lowlight, which default-imports a CommonJS highlight.js module. The Vite client optimizer now pre-bundleslowlight,highlight.js, andhighlight.js/lib/coreso the deep CJS import is wrapped with ESM interop before it reaches the browser. -
#2910
ebd13f8Thanks @MA2153! - Fixes media-usage cleanup reading the entire backlog on every run instead of only the rows it cleans, which made the scheduled cleanup task steadily more expensive as a site's backlog grew. -
#1645
8a06cd6Thanks @mvanhorn! - Fixes admin manifest field mapping so database-backed collections expose field IDs, widget hints, selected validation, SEO flags, and URL patterns with matching public types. -
#2937
4cc3817Thanks @ascorbic! - Fixes Cloudflare Workers requests hanging indefinitely after another request is cancelled while the object cache backend is loading. A timed-out request now bypasses the cache and loads the requested data directly, while later requests can initialize the cache again. -
#2812
d8910d7Thanks @iNerdStack! - Adds anincludeCountsoption togetTerm(), matchinggetTaxonomyTerms(). PassincludeCounts: falseto get a term's label, slug and children without its entry count, which skips the aggregate over the taxonomy's assignments. Counts are still included by default. The built-in category and tag archive pages, which render only the label, opt out. -
#2745
b8873c7Thanks @ascorbic! - AddsPasskeyConfig.userVerificationso sites can require, prefer, or discourage passkey user verification. Existing callers keep thepreferredbehavior.Adds typed, versioned challenge contexts for registration and authentication. Declare a codec with
defineChallengeContext(), bind data withbindChallengeContext()when generating options, and pass the codec with anAtomicChallengeStoretoverifyAuthenticationResponse()orverifyRegistrationResponse()to recover the typed value after verification.Atomic challenge stores declare
readonly atomic: true, so an unrelatedconsume()method on an existing challenge store cannot silently change its behavior. EmDash retains optional challenge context data in its database-backed challenge store.Authentication rejects assertions whose signature counter drops from a nonzero value to zero because the counter change can indicate a cloned authenticator.
-
#2699
01855cbThanks @hossein-webdev! - Fixes plugin storage cursor pagination returning duplicate rows and skipping others wheneverquery()is called withorderBy. The cursor stepped through thecreated_atcolumn while the results were sorted by the requesteddatafield, so the two disagreed: paging newest-first re-returned page one and never reached older rows, and paging ascending broke too whenever the sort field did not happen to match insertion order. Pages now seek on the same expression they are sorted by, andidis appended as a tiebreaker so a page boundary cannot fall inside a group of equal sort values.Documents that omit the sorted field are also paged correctly now. A missing key extracts as NULL, which made every comparison against it UNKNOWN and dropped those rows from later pages. NULLs are given an explicit position in the sort — last when ascending, first when descending — so they land in the same place on SQLite and Postgres instead of following each dialect's own default, and they page through like any other value. If you relied on the previous per-dialect NULL placement for a collection whose documents omit an indexed field, the order of those rows changes.
Paginating with a cursor while sorting several fields in different directions now throws
StorageQueryErrorinstead of silently returning wrong pages. Sort every field the same way, or read the collection without a cursor. -
#2939
c81e5e7Thanks @ascorbic! - Fixes the admin rich-text editor replacing payload-less custom blocks with an[Unknown block type: …]paragraph during autosave. Custom blocks, existing block and span keys, supported marks, and link definitions survive editor round trips, and the editor does not save a synthetic trailing paragraph.Applications using the exported converters can pass
{ preserveIdentity: true }toportableTextToProsemirror()and addportableTextIdentityExtensionsto their TipTap schema for the same lossless behavior. The default conversion remains compatible with standard ProseMirror schemas. -
#2830
965bf33Thanks @khoinguyenpham04! - Fixes image fields and Portable Text editors so they preserve direct image URLs and external provider identities, allowing selected images to continue rendering after saving or replacement. -
#2935
06499adThanks @ascorbic! - Fixes PostgreSQL deployments crashing when an idle pooled connection fails. EmDash now logs the idle-client error without exposing connection credentials while node-postgres discards the failed client and keeps the pool available.The
postgres()adapter'spooloption also acceptsconnectionTimeoutMillisandidleTimeoutMillis. SetconnectionTimeoutMillisto bound how long a request waits for a connection when PostgreSQL is unreachable. Both options remain unset by default, preserving node-postgres's existing timeout behavior. -
#2858
bb8b087Thanks @ascorbic! - Fixes sandboxedcontent:beforeSavehooks being unable to reject content creation or updates.Return a version 1 sandbox hook result with a
SAVE_REJECTEDerror to stop the save and show the reason to the editor:return { __emdashSandboxHookResult: true, version: 1, error: { code: "SAVE_REJECTED", reason: "Add a title before saving.", }, };
The reason must contain 1–500 characters of plain text. Invalid error results and unexpected sandbox exceptions stop the save with a generic hook error instead of exposing internal details.
-
#2913
980538dThanks @htdtkshi! - Fixescontent.schedule()and content updates so offset dates are stored as canonical UTC ISO 8601 timestamps. Positive and negative offsets now publish at the represented instant instead of several hours late or early. -
#2890
30d4076Thanks @MA2153! - Fixes scheduled publishing so its recurring check no longer reads every content entry on each run. Sites running the scheduler on a frequent cron trigger, as the Cloudflare deployment guide recommends, previously saw database reads grow with the size of their content library rather than with the amount of scheduled work — a cost that is directly billable on D1 and was paid even when nothing was scheduled. Existing sites pick up the fix when migrations run on upgrade; no configuration or code changes are needed. -
#2581
9ccc2e7Thanks @emdashbot! - FixesPUT /_emdash/api/schema/collections/{slug}sotitleFieldanddateFieldare no longer silently dropped from the request body. Both fields are now validated, persisted, and returned in the collection response, restoring parity withUpdateCollectionInputand the in-processSchemaRegistrypath. -
#2891
98ef920Thanks @khoinguyenpham04! - Updates the content editor's Publish section so authors can distinguish the live version from draft changes and choose immediate or scheduled publishing from one contextual action menu.Publishing dates and schedules display in the browser's local time zone while stored timestamp values remain unchanged.
Schedule and unschedule responses now return the current revision token so subsequent editor saves retain optimistic-concurrency protection.
-
#2884
2970377Thanks @MA2153! - Fixes the OpenAPI description forGET /_emdash/api/taxonomies/{name}, which said that omittinglocalereturns the lowest-locale definition. The endpoint returns the configured default locale's definition and only falls back to the lowest locale code when the default locale has none. Behavior is unchanged; only the generated API description was wrong. -
#2875
37e08b0Thanks @danielmlr! - Fixes Cloudflare D1 sites that could never finish migrating after migration 017 was interrupted. Every retry failed withtable "_emdash_authorization_codes" already exists; the migration now skips the statements that already ran. -
#2807
013156dThanks @LeanderG! - Fixes the admin Trash tab on multilingual sites, where it listed trashed entries from every locale regardless of the locale picker. Trash now follows the same locale filter as the All tab and shows a Locale column, so switching locales narrows the trash to that locale's entries.GET /_emdash/api/content/{collection}/trashaccepts an optionallocalequery parameter to scope the listing, and each item in the response now carrieslocaleandtranslationGroup. Omittinglocalestill returns every locale, so existing API callers are unaffected. -
Updated dependencies [
76946e4,595a6b1,de8b03a,ecdba4d,6676283,05d5596,66aeecd,529b28b,52fffdc,9def325,85f8b5a,8fb13cf,096cd91,3b124f2,7887577,920e1f3,9a66ff0,87c7884,5f9eb67,8fb13cf,d267a2c,b8873c7,c81e5e7,965bf33,afa81c5,bb8b087,e0e60ba,98ef920,b2da4f2,8fb13cf,c7b6fdf,8efac35,013156d,c7b6fdf]:- @emdash-cms/admin@0.37.0
- @emdash-cms/auth@0.37.0
- @emdash-cms/plugin-types@0.3.1
- @emdash-cms/registry-client@0.5.0
- @emdash-cms/registry-verification@0.3.0
- @emdash-cms/gutenberg-to-portable-text@0.37.0