github emdash-cms/emdash emdash@0.37.0

Minor Changes

  • #2897 f622a17 Thanks @ascorbic! - Adds id to the content:beforeSave hook event when an existing item is updated, for both trusted and sandboxed plugins. event.content holds only the submitted field values, so a hook that needs the stored item, for example to audit or validate a change against it, can call ctx.content.get(event.collection, event.id). The field is absent on creates.

  • #2899 595a6b1 Thanks @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 05d5596 Thanks @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 6da29d3 Thanks @danielmlr! - Breaking (MCP clients): Requires _rev on the MCP content_update, content_publish, content_unpublish and content_discard_draft tools, so an agent can no longer write over changes it never read. The CLI has always required the token on content update; the MCP surface now matches it.

    The four tools previously accepted _rev as an optional parameter and performed the write when it was omitted. Such a call now fails validation with a message naming content_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 556c9fe Thanks @emdashbot! - Adds MCP tools for managing taxonomy definitions: taxonomy_get, taxonomy_create, taxonomy_update, and taxonomy_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_create accepts name, label, labelSingular, hierarchical, collections, locale, and translationOf. When translationOf is used and hierarchical or collections are omitted, the new definition inherits them from the source taxonomy, fixing the defaulting trap described in #2525.

  • #2746 c7b6fdf Thanks @ascorbic! - Adds DirectPdsClient for 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_MISMATCH before 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 inspectPackageReleaseRecords for 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-256 multihashes 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 76946e4 Thanks @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 ad19827 Thanks @ismail-rt! - Fixes the Archives widget so monthly and yearly lists include posts whose publishedAt value is a Date object instead of rendering an empty list.

  • #2783 cd294dc Thanks @yumam0815! - Fixes publication workflows so callers can pass the approved _rev to publish, unpublish, or discard a draft and receive a CONFLICT response when the entry changed.

  • #2852 b06fc63 Thanks @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 ecdba4d Thanks @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 7a5d9c1 Thanks @MA2153! - Fixes byline profile pages having no way to render the byline's avatar (#2613). getByline, getBylineBySlug, and the underlying single-row BylineRepository finders now resolve the avatar's media row in the same query, so avatarStorageKey, avatarAlt, avatarBlurhash, and avatarDominantColor are populated alongside avatarMediaId:

    ---
    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 a LEFT JOIN, not a second round trip), and findMany still skips the join, so byline list pages are unaffected.

  • #2886 de122b4 Thanks @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 6676283 Thanks @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 d418b64 Thanks @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 *WithCacheHint variants that return page-level cache hints. Stable invalidation tags are emdash:settings, emdash:menu:<name>, emdash:taxonomy:<name> and emdash:widget-area:<name>.

  • #2822 60691df Thanks @jcheese1! - Fixes getEmDashCollection() and getEmDashEntry() returning SQLite-backed boolean fields as 0 or 1. Boolean fields now return true or false, matching their generated TypeScript types, while integer fields retain numeric values.

  • #2936 062e8be Thanks @emdashbot! - Regenerate emdash-env.d.ts immediately when the schema changes during astro dev, so newly added fields and collections are available in types without reloading the dev server.

  • #2925 b44bc2c Thanks @emdashbot! - Fixes the OpenAPI document for content taxonomy terms so it matches the shipped route. The documented PUT /_emdash/api/content/{collection}/{id}/terms path has been removed; use GET or POST /_emdash/api/content/{collection}/{id}/terms/{taxonomy} instead. The taxonomy path parameter is required, and POST validates that every term id belongs to that taxonomy.

  • #2900 9def325 Thanks @khoinguyenpham04! - Fixes Cloudflare development servers failing during cold start with Astro 7.3.1 after Vite discovers astro/logger/console and invalidates prebundled server chunks.

  • #2921 67f676d Thanks @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-bundles lowlight, highlight.js, and highlight.js/lib/core so the deep CJS import is wrapped with ESM interop before it reaches the browser.

  • #2910 ebd13f8 Thanks @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 8a06cd6 Thanks @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 4cc3817 Thanks @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 d8910d7 Thanks @iNerdStack! - Adds an includeCounts option to getTerm(), matching getTaxonomyTerms(). Pass includeCounts: false to 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 b8873c7 Thanks @ascorbic! - Adds PasskeyConfig.userVerification so sites can require, prefer, or discourage passkey user verification. Existing callers keep the preferred behavior.

    Adds typed, versioned challenge contexts for registration and authentication. Declare a codec with defineChallengeContext(), bind data with bindChallengeContext() when generating options, and pass the codec with an AtomicChallengeStore to verifyAuthenticationResponse() or verifyRegistrationResponse() to recover the typed value after verification.

    Atomic challenge stores declare readonly atomic: true, so an unrelated consume() 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 01855cb Thanks @hossein-webdev! - Fixes plugin storage cursor pagination returning duplicate rows and skipping others whenever query() is called with orderBy. The cursor stepped through the created_at column while the results were sorted by the requested data field, 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, and id is 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 StorageQueryError instead of silently returning wrong pages. Sort every field the same way, or read the collection without a cursor.

  • #2939 c81e5e7 Thanks @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 } to portableTextToProsemirror() and add portableTextIdentityExtensions to their TipTap schema for the same lossless behavior. The default conversion remains compatible with standard ProseMirror schemas.

  • #2830 965bf33 Thanks @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 06499ad Thanks @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's pool option also accepts connectionTimeoutMillis and idleTimeoutMillis. Set connectionTimeoutMillis to 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 bb8b087 Thanks @ascorbic! - Fixes sandboxed content:beforeSave hooks being unable to reject content creation or updates.

    Return a version 1 sandbox hook result with a SAVE_REJECTED error 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 980538d Thanks @htdtkshi! - Fixes content.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 30d4076 Thanks @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 9ccc2e7 Thanks @emdashbot! - Fixes PUT /_emdash/api/schema/collections/{slug} so titleField and dateField are no longer silently dropped from the request body. Both fields are now validated, persisted, and returned in the collection response, restoring parity with UpdateCollectionInput and the in-process SchemaRegistry path.

  • #2891 98ef920 Thanks @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 2970377 Thanks @MA2153! - Fixes the OpenAPI description for GET /_emdash/api/taxonomies/{name}, which said that omitting locale returns 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 37e08b0 Thanks @danielmlr! - Fixes Cloudflare D1 sites that could never finish migrating after migration 017 was interrupted. Every retry failed with table "_emdash_authorization_codes" already exists; the migration now skips the statements that already ran.

  • #2807 013156d Thanks @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}/trash accepts an optional locale query parameter to scope the listing, and each item in the response now carries locale and translationGroup. Omitting locale still 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

Don't miss a new emdash release

NewReleases is sending notifications on new releases.