Routine maintenance and dependency updates.
Release history
emdash releases
All releases
222 shown
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Environment requirement declaration
Environment checks + assets + sections
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Environment deps + assets + translations
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Sandboxing + storage fixes + kysely upgrade
Kumo bump + kysely security upgrade
i18n bylines + API + repo + auth + kysely
Byline i18n + code picker + plugin management
workerd sandbox + storage fix + kysely bump
Routine maintenance and dependency updates.
Response validation & refined types
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Media ID resolution + Registry fixes + Aggregator validation
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Menu API enforcement + camelCase
- Removed the named export `atprotoPlugin` and its factory call shape; import the default export (`import atproto from "@emdash-cms/plugin-atproto"`) and pass it directly to `sandboxed:` or `plugins:`.
Full changelog
Minor Changes
-
#1057
c0ce915Thanks @ascorbic! - BREAKING: Removes theatprotoPluginnamed export and the factory call shape. Import the default export and pass it directly intoplugins:orsandboxed:.- import { atprotoPlugin } from "@emdash-cms/plugin-atproto"; + import atproto from "@emdash-cms/plugin-atproto"; export default defineConfig({ integrations: [ emdash({ - sandboxed: [atprotoPlugin()], + sandboxed: [atproto], }), ], });Two changes: drop the
{ }around the import, and drop the()after the plugin name. Per-install configuration moved to the admin UI's settings (KV-backed) when the sandboxed plugin redesign landed, so there's no longer a need for a factory call.
Patch Changes
Routine maintenance and dependency updates.
- Removed named export `webhookNotifierPlugin`; import the default export and pass directly.
Full changelog
Minor Changes
-
#1057
c0ce915Thanks @ascorbic! - BREAKING: Removes thewebhookNotifierPluginnamed export and the factory call shape. Import the default export and pass it directly intoplugins:orsandboxed:.- import { webhookNotifierPlugin } from "@emdash-cms/plugin-webhook-notifier"; + import webhookNotifier from "@emdash-cms/plugin-webhook-notifier"; export default defineConfig({ integrations: [ emdash({ - sandboxed: [webhookNotifierPlugin()], + sandboxed: [webhookNotifier], }), ], });Two changes: drop the
{ }around the import, and drop the()after the plugin name. Per-install configuration moved to the admin UI's settings (KV-backed) when the sandboxed plugin redesign landed, so there's no longer a need for a factory call.
Patch Changes
- Removed `auditLogPlugin` named export and factory call shape; use default import instead.
Full changelog
Minor Changes
-
#1057
c0ce915Thanks @ascorbic! - BREAKING: Removes theauditLogPluginnamed export and the factory call shape. Import the default export and pass it directly intoplugins:orsandboxed:.- import { auditLogPlugin } from "@emdash-cms/plugin-audit-log"; + import auditLog from "@emdash-cms/plugin-audit-log"; export default defineConfig({ integrations: [ emdash({ - plugins: [auditLogPlugin()], + plugins: [auditLog], }), ], });Two changes: drop the
{ }around the import, and drop the()after the plugin name. Per-install configuration moved to the admin UI's settings (KV-backed) when the sandboxed plugin redesign landed, so there's no longer a need for a factory call.
Patch Changes
Routine maintenance and dependency updates.
- Sandboxed plugins must update imports to use `import type { SandboxedPlugin } from "emdash/plugin"` and add the `satisfies SandboxedPlugin` assertion.
- Adjust any code that relies on snake_case keys in menu API responses to camelCase (e.g., `createdAt`, `menuId`).
- Update handlers for menu item CRUD endpoints to use path‑parameter style URLs (`/menus/:name/items/:id`).
- Removed `definePlugin` helper for sandboxed-format plugins; new shape is a bare default export with `satisfies SandboxedPlugin` annotation.
- `POST /menus/:name/items` now rejects unknown keys and enforces strict validation, returning 400 on unrecognized fields.
- Menu item CRUD endpoints switched from query‑string `?id=` to path parameters (`PUT/DELETE /menus/:name/items/:id`).
- Experimental support for a decentralized plugin registry via `experimental.registry.aggregatorUrl` in `astro.config.mjs`.
- Added per‑plugin configuration options: `policy.minimumReleaseAge` and `policy.minimumReleaseAgeExclude`.
Full changelog
Minor Changes
-
#1057
c0ce915Thanks @ascorbic! - BREAKING (plugin authors): Reworks how sandboxed plugins are defined. ThedefinePlugin()helper is removed for sandboxed-format plugins; the new shape is a bare default export with asatisfies SandboxedPluginannotation. A new type-only subpathemdash/pluginprovides the types.This affects anyone writing a sandboxed plugin. Sites that use plugins are unaffected (see the per-plugin changesets for the import-shape change in published plugins).
- import { definePlugin, type ContentHookEvent, type PluginContext } from "emdash"; + import type { SandboxedPlugin } from "emdash/plugin"; - export default definePlugin({ + export default { hooks: { "content:beforeSave": { - handler: async (event: ContentHookEvent, ctx: PluginContext) => { + handler: async (event, ctx) => { // ... return event.content; }, }, }, - }); + } satisfies SandboxedPlugin;Three changes:
- Drop
import { definePlugin } from "emdash"and thedefinePlugin(...)wrapping call. Sandboxed plugins now default-export the bare object. import type { SandboxedPlugin } from "emdash/plugin"and addsatisfies SandboxedPluginto the default export. Theemdash/pluginsubpath is type-only — the bundler erases the import, so no runtime resolution ofemdashis needed (and the heavyemdashruntime no longer enters the plugin bundle).- Drop handler parameter annotations like
event: ContentSaveEvent, ctx: PluginContext. The strict mapped type onSandboxedPlugininfers them per hook name, with the full canonical event type. If you need to reference an event type by name (e.g. in a helper function),emdash/pluginre-exports them:import type { ContentHookEvent, PluginContext } from "emdash/plugin".
Why: the old
definePluginwas an identity function whose only job was to aliasemdashto a Proxy shim at build time so the import would resolve. With the new shape, sandboxed plugins have no runtimeemdashimport — only type-only imports fromemdash/plugin. The bundler doesn't need to alias anything; the build pipeline is simpler; and authors get strict per-hook event/return type inference for free.The trade-off: previously you could narrow an event type locally (e.g.
interface ContentSaveEvent { content: ... & { id: string } }). Under the strict mapped type, the canonical event type wins (TypeScript's contravariance on function parameters means narrowing isn't assignable). Authors validate fields at runtime withtypeof/isRecordchecks instead — which is the right pattern for input that comes from outside the type system anyway.Routes follow the same simplification. The two-arg
(routeCtx, ctx)shape is unchanged; only the annotations disappear:export default { routes: { health: async (routeCtx, ctx) => { // routeCtx: SandboxedRouteContext, ctx: PluginContext — both inferred. return new Response("ok"); }, }, } satisfies SandboxedPlugin;SandboxedRouteContextexposes{ input, request, requestMeta? }.requestis typed asSandboxedRequest— a{ url, method, headers }record that's portable across in-process and isolate execution (Worker Loader can't pass realRequestobjects across the boundary).Native plugins are unaffected. This change applies only to sandboxed-format plugins. Native plugins continue to use
definePlugin()fromemdashand the existingPluginDefinitionshape.Type rename:
SandboxedPluginon theemdashpackage now refers to the new author-facing source-shape type. The runtime-side handle type (returned bySandboxRunner.load, held in the runtime's plugin cache) is renamed toSandboxedPluginInstance. If you importSandboxedPluginfromemdashto type a sandbox runner implementation or hold runtime plugin handles, update those imports toSandboxedPluginInstance. Public consumers of this type are mostly limited to@emdash-cms/cloudflareand other sandbox runner adapters; standard plugin / site code is unaffected.Removed types:
StandardPluginDefinition,StandardHookHandler,StandardHookEntry,StandardRouteHandler,StandardRouteEntryare no longer exported fromemdash. These were authoring-helper aliases under the old permissivedefinePluginstandard overload. UseSandboxedPluginfromemdash/pluginfor the same purpose under the new shape.Removed function:
isStandardPluginDefinitionis gone. There's no equivalent — sandboxed plugins are identified by structure ({ hooks?, routes? }) and you should treat the default export as already typed viasatisfies SandboxedPlugin. - Drop
-
#1052
0d5843fThanks @Rimander! - Fixes menu REST API consistency:POST /menus/:name/itemsno longer accepts unknown keys silently. Sendingcustom_url(snake_case) orurlused to return 201 withcustom_url: nullbecause Zod's default.strip()quietly dropped them. The schemas now use.strict()and return 400VALIDATION_ERRORwithUnrecognized key: "custom_url". The documented camelCase keys (customUrl,sortOrder,referenceCollection, etc.) are unchanged and persist as before. Thetypefield is now validated against the canonical enum ("custom" | "page" | "post" | "taxonomy" | "collection"); previously any string passed.- Moves per-item writes to
PUTandDELETE /menus/:name/items/:id(path-style). Every other EmDash resource (content,taxonomies,redirects,sections,widget-areas) addresses items by URL path; menus were the lone outlier requiring?id=<id>in the query string. The legacy query-string form is removed (it was undocumented and only used by the admin, which is updated in this PR). Callers should usePUT /menus/:name/items/:id/DELETE /menus/:name/items/:id. - Menu and menu-item API responses are now camelCase, aligning with the rest of EmDash's REST surface (
content,taxonomies,redirects, …).created_at→createdAt,updated_at→updatedAt,menu_id→menuId,parent_id→parentId,sort_order→sortOrder,reference_collection→referenceCollection,reference_id→referenceId,custom_url→customUrl,title_attr→titleAttr,css_classes→cssClasses,translation_group→translationGroup. Breaking for direct REST consumers that depend on snake_case keys in the response body. The admin UI is already updated. - Refactors menus to the standard repository pattern. Adds
MenuRepositorynext toContentRepository,TaxonomyRepository,RedirectRepository,MediaRepository,CommentRepository. Handlers become thin orchestrators; the repository is now the single place where snake_case rows become camelCase entities.
These changes do not touch any database schema or migration. Existing data is preserved.
-
#1011
dbaea9cThanks @ascorbic! - Adds experimental support for the decentralized plugin registry (see RFC #694). Configure withexperimental.registry.aggregatorUrlinastro.config.mjs; the admin UI then uses the registry instead of the centralized marketplace for browse and install. Marketplace behavior is unchanged when the option is not set.The experimental config accepts a
policy.minimumReleaseAgeduration (e.g."48h") that holds back releases below that age from install and update prompts, with apolicy.minimumReleaseAgeExcludeallowlist for trusted publishers or specific packages. The minimum-release-age check is enforced both client-side (for UX) and server-side (in the install endpoint), so stale browser tabs and deep links still hit the gate.
Patch Changes
-
#1076
6e62b90Thanks @ascorbic! - Fixes spurious TypeScript errors in strict projects that consume EmDash. Several subpaths (emdash/routes/*,emdash/api/route-utils,emdash/api/schemas,emdash/auth/providers/github,emdash/auth/providers/google) previously shipped raw source, so yourtscand editor type-checked EmDash's internals against your config and could report errors that weren't yours. These now ship compiled type declarations instead. The*-adminproviders andemdash/uistay source because they bridge the admin React/Astro runtime your own build processes. Import paths and runtime behaviour are unchanged. -
#1086
23597d0Thanks @ascorbic! - Fixes silent data loss in migration 036 on Cloudflare D1 (#1021). D1 ignoresPRAGMA foreign_keys = OFFand its replacementdefer_foreign_keysonly defers constraint validation, it doesn't suppress CASCADE actions, so dropping any table during the i18n rebuild fired its child cascades. Three FK relationships were affected:content_taxonomies.taxonomy_id -> taxonomies(id) ON DELETE CASCADEwiped all post-taxonomy associations.taxonomies.parent_id -> taxonomies(id) ON DELETE SET NULLflattened taxonomy hierarchies._emdash_menu_items.menu_id -> _emdash_menus(id) ON DELETE CASCADEwiped every menu item on the install (along withparent_id -> _emdash_menu_items(id) ON DELETE CASCADEmopping up nested items).
The migration now physically removes those FK relationships before any drop.
content_taxonomiesand_emdash_menu_itemsare rebuilt without their parent FKs as the first steps of up(), and the newtaxonomiesself-FK targets its temporary name (taxonomies_new) which SQLite rebinds on RENAME. The FKs from migration 005 on_emdash_menu_itemsare not restored on rollback either: the runtime always deleted child rows explicitly, so the cascade was redundant and reinstating it would only re-create the #1021 hazard on any future migration that drops_emdash_menus. Rollback also refuses to run whencontent_taxonomieshas rows referencing translation groups with no survivingtaxonomiesrow, surfacing dangling data before any destructive work, and theidx_content_taxonomies_termindex from migration 015 is restored after each rebuild.This is forward-fix only. Installs that already lost data when running 036 will need to restore from D1 Time Travel.
-
#1088
883b75bThanks @MA2153! - FixesEmDashClient.terms()returning{ terms }instead of{ items }, which causedpage.itemsto beundefinedfor any caller that iterated the result. The API handler returns{ terms: TermWithCount[] }but the client was typed and advertised asListResult<Term>— the key name mismatch is now mapped correctly. -
#751
05440b1Thanks @edrpls! - Fix the admin collection list pagination denominator so it no longer grows in increments of 5 as the user pages forward.The
GET /_emdash/api/content/{collection}response now includes atotalfield with the full filtered row count (independent oflimit). The admin uses it as the pagination denominator, so a 143-entry collection reads1/8on page 1 instead of1/5 → 5/10 → 10/15 → …as successive API pages load.The
totalfield is optional; pre-upgrade clients that ignore it still work, and the admin falls back to the loaded-item count when an older server doesn't return it.Also handles the edge case where the current page exceeds
totalPagesafter filtering or deletion — the admin clamps the active page so the table doesn't render empty while waiting for a refetch. -
#1000
94fb50bThanks @ask-bonk! - Fixes invite passkey registration behind a TLS-terminating reverse proxy. The inviteregister-optionsendpoint now resolves the public origin viagetPublicOrigin(url, emdash.config)before callinggetPasskeyConfig, matching every other passkey endpoint. Previously the WebAuthn RP ID fell back tourl.hostname(e.g.localhost), causing the browser to reject the registration with "Security error" when the public origin differed from the upstream host. -
#1013
0cd8c6dThanks @ascorbic! - Fixes the slash command menu's initial selection getting overridden when the menu opens under a stationary pointer. The menu items previously reacted tomouseenterunconditionally, so an item rendered beneath the cursor would steal selection from the keyboard default before any user interaction. Mouse-hover-selects still works, but only after the user actually moves the pointer over the menu. -
#1087
878a0b6Thanks @ascorbic! - Fixes two data-loss bugs in the WordPress WXR import path (admin UI Settings, Import, WordPress, i.e.POST /_emdash/api/import/wordpress/execute).Per-post taxonomy assignments parsed from
<wp:category>,<wp:tag>,<wp:term>, and per-item<category domain="...">blocks (#1061) are now persisted. The HTTP execute handler previously extracted this data and silently discarded it before any taxonomy or pivot rows were written. Terms are created idempotently in EmDash's seededcategoryandtagtaxonomies; custom taxonomies such asgenreare matched against existing EmDash definitions via the runtime's locale fallback chain (resolveLocaleChain), so imports against a non-default-locale site reuse defs seeded at the default locale instead of false-failing. Unknown custom taxonomies surface in a newresult.taxonomies.missingTaxonomiesfield instead of being silently dropped, so the admin can prompt the user to create the missing definition. Assignments respect each taxonomy definition'scollectionsarray.WPML and Polylang translations (#1080) are now imported under their own per-post locale and linked via
translation_group. Previously the entire upload shared oneconfig.localeand the second post of any translation pair was rejected by theUNIQUE(slug, locale)constraint introduced in migration 019. The parser promotes per-post locale from_icl_lang_code(WPML),trid(WPML's translation group id),_locale(Polylang), thelanguagetaxonomy, or_translationspostmeta. Terms are mirrored into each translation's locale so per-locale lookups (getTermsForEntry(..., locale)) resolve correctly on every translation row. Per-translation taxonomy assignments override anchor-inherited ones per-taxonomy when the translator picked different terms, matching WPML "Translate Independently" mode. Taxonomies the translation did not touch keep their inherited assignments, matching WPML "Sync" mode and Polylang's default.Adds
result.taxonomiesto the import response (additive). Existing consumers continue to work unchanged.Scope note: this fixes the HTTP import path, which is what the admin UI calls. The standalone
emdash import wordpressCLI command writes JSON files to disk and has its own slug-only output path that does not carry locale, so it can still clobber two translations with the samepost_name. That is a separate fix and not addressed here. -
#768
121f173Thanks @ask-bonk! - FixesSQLITE_CORRUPT_VTAB(database disk image is malformed) when editing or publishing content on collections that have search enabled, and on restore-from-trash, permanent-delete, and edit-while-trashed flows.The FTS5 sync triggers used the contentless-table form (
DELETE FROM fts WHERE rowid = OLD.rowid) on what is actually an external-content FTS5 table. After an UPDATE onec_<collection>, FTS5 then read NEW column values from the (already updated) content table while trying to remove OLD tokens from the inverted index, drifting the index out of sync until SQLite refused further reads. Rewrites the triggers to use the documented external-content-safeINSERT INTO fts(fts, rowid, ...) VALUES('delete', OLD.rowid, OLD.col1, ...)pattern, gated onOLD.deleted_at IS NULLso we don't try to remove rows that were never indexed (which would itself raiseSQLITE_CORRUPT_VTABon restore-from-trash and permanent-delete).Adds migration
039_fix_fts5_triggersthat rebuilds the FTS index for every search-enabled collection on upgrade, replacing the broken triggers and recovering from any latent index corruption left behind by earlier mutations. The migration runs once at startup before the first request can hit the affected paths, so upgrading sites get the fix on their next deploy without depending on a search-endpoint visit to trigger lazy auto-repair. -
#1077
f4a9711Thanks @ascorbic! - FixesAstro.locals.emdashtyping. The shipped type declaration referenced a build artifact that does not exist, solocals.emdashsilently fell back toanyin every EmDash site — losing autocomplete and type-checking on the handlers API in your pages and endpoints. It is now correctly typed asEmDashHandlers. -
#1019
5681eb2Thanks @ascorbic! - Fixes a Zod type-incompatibility between trusted plugins and core. Without a workspace-level pin, emdash'szod: ^4.3.5could resolve to a different patch than Astro's bundled Zod, and Zod 4 embeds the version in the type — so schemas imported viaastro/zodin trusted plugins (e.g.@emdash-cms/plugin-forms) were not assignable todefinePlugin'sPluginRoute<TInput>['input']. Pins Zod in the pnpm catalog so the entire workspace dedupes on one instance. -
#1074
ed917d9Thanks @ascorbic! - Fixes stored config sharing when the runtime module is loaded as both compileddistand rawsrcin the same process (Vite SSR / dual-package). The integration config is now keyed on a globalSymbol.forregistry entry instead of a typedglobalThisvar, matching the existing isolate-singleton pattern, sogetStoredConfig()resolves consistently across both module copies. -
#1076
6e62b90Thanks @ascorbic! - Fixes a type error in the shipped WordPress-plugin import source: the analyze-endpoint error body fromresponse.json()isunknownunder@cloudflare/workers-typesand was read without narrowing. This file ships as raw source via theemdash/routes/*export, so the error surfaced in strict consumer typechecks (issue #1053). The body is now typed before.messageis read; runtime behaviour is unchanged. -
Updated dependencies [
05440b1,484e7ab,0d5843f,0cd8c6d,d014b48,dbaea9c,5681eb2]:- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
Manifest support + rename + flag changes
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Routine maintenance and dependency updates.
Fixed TypeError crash on content mutation API routes when Astro's cache is disabled.
Full changelog
Patch Changes
-
#991
dc44989Thanks @ascorbic! - Fixes TypeError crash on all content mutation API routes when Astro's cache API is not available. Thecachecontext parameter is undefined when the cache feature is not enabled, causingcache.enabledto throw. All 11 call sites now use optional chaining (cache?.enabled). -
Updated dependencies []:
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- Run `ALTER TABLE credentials ADD COLUMN algorithm INTEGER NOT NULL DEFAULT -7` manually if using standalone `@emdash-cms/auth` with an existing `credentials` table.
- Migration `016_api_tokens` now safely retries without error on partially‑applied runs.
- Enforces strict sandboxed plugin bundle limits: total decompressed ≤256 KB, per‑file ≤128 KB, max 20 files; bundles sized 256 KB–5 MB are now rejected.
- `image` builder's `allowedTypes` option is now load‑bearing and enforces MIME type filtering.
- Per‑field allowed MIME types for `file` and `image` fields with schema editor UI support
- Passkey authentication now supports RS256 (RSA) in addition to ES256 (ECDSA)
Full changelog
Minor Changes
-
#978
27e6d58Thanks @ascorbic! - Enforces the sandboxed plugin bundle size caps from RFC 0001 §"Bundle size limits" in both thebundleandpublishCLI flows: total decompressed ≤ 256 KB, per-file decompressed ≤ 128 KB, and at most 20 files per bundle. The previous bundle command capped only the total at 5 MB; the publish command now also re-validates the decompressed tarball before signing the release record so a publisher hits the same cap locally that aggregators enforce at ingest. Bundles between 256 KB and the old 5 MB ceiling will now be rejected — usually a sign the plugin is bundling host-provided dependencies or assets that belong in a CDN rather than the plugin payload. -
#942
7c536e5Thanks @MA2153! - Adds per-field allowed MIME types forfileandimagefields. Field-levelallowedTypesis now honored end-to-end: it filters the media picker, widens upload acceptance for that field (so e.g. a zip-only field can accept zip uploads even though the global allowlist excludes them), and validates referenced media against the destination field on content save. The schema editor in admin gains an "Allowed types" control with curated presets and freeform entry.Behavior change: the
imagebuilder'sallowedTypesoption was previously accepted but read by nothing. It is now load-bearing — a code-first schema that already passedallowedTypes(e.g.["image/png"]) will now actually narrow the picker and gate uploads. Most users will see no change; if you set this option intending the old (silent) behavior, drop it.Behavior change: updating a field via the admin schema editor now explicitly clears its validation when the form contains no validation settings, instead of leaving an existing
validationvalue intact. This only affects fields with pre-existing validation that is not expressible in the editor UI.
Patch Changes
-
#893
f8ee1edThanks @j-liszt! - Enhances Passkey authentication with polymorphic algorithm support. Adds support for RS256 (RSA) alongside the existing ES256 (ECDSA) implementation, ensuring full compatibility with Windows Hello, hardware security keys, and FIDO2 standards. Includes a database migration to track and persist credential algorithms for future-proof authentication.Note for standalone
@emdash-cms/authconsumers: If yourcredentialstable already exists, you must manually runALTER TABLE credentials ADD COLUMN algorithm INTEGER NOT NULL DEFAULT -7to support this update. TheDEFAULT -7value ensures that existing rows (which are all ES256) continue to work seamlessly without requiring any data backfill. -
#976
4c11017Thanks @ask-bonk! - Fixes migration016_api_tokensfailing withtable "_emdash_api_tokens" already existsafter a partially-applied previous attempt. Ifup()crashed mid-way (D1 subrequest limit, isolate cancellation, transient connection error), the migration record never got recorded and Kysely re-ran the migration from the top on the next request, blocking every subsequent boot.up()now usesIF NOT EXISTSon every CREATE so a retry skips already-applied steps and finishes the remainder. Resolves the "table already exists" error reported on fresh Cloudflare Workers + D1 deploys. -
#939
f1d4c0bThanks @schiste! - Make the MCP menu write tools locale-aware by exposinglocaleonmenu_create,
menu_update,menu_delete, andmenu_set_items, exposingtranslationOfon
menu_create, and teachinghandleMenuSetItems()to target the requested locale
and tag inserted menu items with that menu's locale.All seven menu-name lookups (
handleMenuUpdate,handleMenuDelete,
handleMenuSetItems,handleMenuItemCreate,handleMenuItemUpdate,
handleMenuItemDelete,handleMenuItemReorder) now fail loud with the new
AMBIGUOUS_LOCALEerror code (HTTP 400) when called with anamethat exists
in multiple locales and nolocaleis provided. Previously the lookup silently
picked an arbitrary translation, which could rewrite or delete the wrong
locale's menu on multi-locale installs. The error message lists the available
locales so callers can recover. Single-locale installs and callers that already
passlocaleare unaffected.The
translationOf→localerequirement is now enforced inside
handleMenuCreate(returnsVALIDATION_ERROR), so REST/SDK callers get the
same guard the MCP boundary already provided. -
d273e9aThanks @ascorbic! - Refactors the plugin manifest types to re-export from@emdash-cms/plugin-types. The capability vocabulary (PluginCapability,CAPABILITY_RENAMES,normalizeCapability,isDeprecatedCapability) and manifest shape (ManifestHookEntry,ManifestRouteEntry,PluginStorageConfig,StorageCollectionConfig) now live in the shared package so the registry CLI can write the same types core reads. Existing imports fromemdash's plugin types module continue to work unchanged. -
#943
514d32dThanks @Rimander! - Fixes seed menu items losing theirtranslation_groupacross export/apply by adding optionalid,locale, andtranslationOffields toSeedMenuItem. The export emits stable seed IDs andtranslationOfreferences; the apply resolves them to the anchor'stranslation_group, matching the existing pattern for content entries, taxonomies, and terms. -
#948
8116949Thanks @ascorbic! - Adds always-ondb.*andcache.*Server-Timing fields so render-phase performance is diagnosable in production. Each request now emitsdb.total(cumulative DB ms),db.count(query count),db.first/db.last(first/last query offset from request start), andcache.hit/cache.miss(request-scoped cache stats). The Kysely log hook is now always installed so counters work without settingEMDASH_QUERY_LOG. -
#946
c4ee7adThanks @LeanderG! - Fixes Postgres rate-limit queries by quoting the reservedwindowcolumn name. -
Updated dependencies [
7f6b6ea,131bea6,f8ee1ed,54b5aa1,c630e31,7c536e5,7aa1897,943df46,0b8a319,13ff061,49b66d9,1b2fa77,530b013,af15975,a4968c1,f80fb58]:- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- Adds @emdash-cms/registry-client: atproto-aware plugin registry client with credential storage, publisher repo ops, and discovery layers (EXPERIMENTAL)
Full changelog
Patch Changes
-
#923
943df46Thanks @ascorbic! - Adds@emdash-cms/registry-client: atproto-aware client for the EmDash plugin registry. Three independent layers — credential storage (filesystem / env-vars / in-memory), publisher repo operations, and discovery against an aggregator. EXPERIMENTAL — pin to an exact version while RFC 0001 is in flight. -
Updated dependencies [
5464b55,943df46]:- @emdash-cms/[email protected]
Fix embedded forms to accept the API response envelope returned by the form definition route.
Full changelog
- Enforces sandboxed plugin bundle total decompressed ≤256 KB, per‑file ≤128 KB, and at most 20 files; bundles between 256 KB and the previous 5 MB limit are now rejected.
Full changelog
Minor Changes
- #978
27e6d58Thanks @ascorbic! - Enforces the sandboxed plugin bundle size caps from RFC 0001 §"Bundle size limits" in both thebundleandpublishCLI flows: total decompressed ≤ 256 KB, per-file decompressed ≤ 128 KB, and at most 20 files per bundle. The previous bundle command capped only the total at 5 MB; the publish command now also re-validates the decompressed tarball before signing the release record so a publisher hits the same cap locally that aggregators enforce at ingest. Bundles between 256 KB and the old 5 MB ceiling will now be rejected — usually a sign the plugin is bundling host-provided dependencies or assets that belong in a CDN rather than the plugin payload.
Patch Changes
-
#929
5464b55Thanks @ascorbic! - Fixes the CLI hanging indefinitely after a successfulloginorlogout.run()was returning correctly, but something in the OAuth path left a ref'd handle alive that prevented Node's event loop from draining. Workaround: force-exit at the top level oncerunMainresolves. The underlying handle leak is unidentified. -
#929
5464b55Thanks @ascorbic! - Switches the login flow to request granular OAuth scopes derived from the@emdash-cms/registry-lexiconslexicon set instead of the broadtransition:generic:repo:for every record-shaped lexicon (package profile, package release, publisher profile, publisher verification) andrpc:<nsid>?aud=*for every aggregator query (getLatestRelease,getPackage,listReleases,resolvePackage,searchPackages). Display name resolution no longer goes throughcom.atproto.server.getSession; the handle is read from the DID document viaLocalActorResolverso the CLI doesn't need anrpc:com.atproto.*scope and isn't affected by PDS-side DPoP/Bearer compatibility quirks. If the PDS rejects the granular scopes withinvalid_scope, login automatically retries once withtransition:genericand prints a notice. Existing sessions continue working with their original scope until they're revoked or re-issued. -
#929
5464b55Thanks @ascorbic! - Improvesloginerror reporting for OAuth response failures. Previously, transient PDS errors surfaced as a bareunknown_errorwith a stack trace; the CLI now prints the HTTP status, endpoint, OAuth error code/description, a body snippet when the response wasn't OAuth-shaped JSON, and a hint to retry on 5xx responses. -
#923
943df46Thanks @ascorbic! - Adds@emdash-cms/registry-cli: standalone CLI for the experimental plugin registry. Subcommands forlogin,logout,whoami,switch,search,info,bundle, andpublish. Atproto OAuth via loopback callback server. Thepublishflow fetches the tarball from the URL, verifies a sha256 multihash, extracts and validatesmanifest.json, locally validates each lexicon record, and atomically writes profile + release records (with the EmDash declaredAccess trust extension) via a single atprotoapplyWrites. Distributes vianpx @emdash-cms/registry-clito keep atproto deps out of the core CMS install. -
Updated dependencies [
943df46,943df46,5464b55,943df46]:- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- Adds `RECORD_NSIDS` and `QUERY_NSIDS` const arrays enumerating record-shaped and query-shaped lexicons.
- Introduces generated TypeScript types and runtime validation schemas for EmDash plugin registry lexicons (`com.emdashcms.experimental.*`).
Full changelog
Minor Changes
- #929
5464b55Thanks @ascorbic! - AddsRECORD_NSIDSandQUERY_NSIDSconst arrays alongside the existingNSIDmap. They enumerate the record-shaped and query-shaped lexicons in this package so consumers (e.g. tooling that builds OAuthrepo:/rpc:scopes) can derive their list from the lexicon set instead of hand-rolling one that drifts.
Patch Changes
- If you previously set `allowedTypes` on an image field expecting silent behavior, remove it to avoid unintended upload filtering.
- Per‑field `allowedTypes` for `file` and `image` fields (schema editor gets "Allowed types" control)
- Table support in PortableText editor (/table command, bubble menu)
Full changelog
Minor Changes
-
#942
7c536e5Thanks @MA2153! - Adds per-field allowed MIME types forfileandimagefields. Field-levelallowedTypesis now honored end-to-end: it filters the media picker, widens upload acceptance for that field (so e.g. a zip-only field can accept zip uploads even though the global allowlist excludes them), and validates referenced media against the destination field on content save. The schema editor in admin gains an "Allowed types" control with curated presets and freeform entry.Behavior change: the
imagebuilder'sallowedTypesoption was previously accepted but read by nothing. It is now load-bearing — a code-first schema that already passedallowedTypes(e.g.["image/png"]) will now actually narrow the picker and gate uploads. Most users will see no change; if you set this option intending the old (silent) behavior, drop it.Behavior change: updating a field via the admin schema editor now explicitly clears its validation when the form contains no validation settings, instead of leaving an existing
validationvalue intact. This only affects fields with pre-existing validation that is not expressible in the editor UI. -
#921
530b013Thanks @jcheese1! - Adds table support to the PortableText editor. Users can now insert and edit tables via the slash command menu (/table) or toolbar button. Tables support header rows, column/row insertion and deletion, and include a bubble menu for quick editing.
Patch Changes
-
#958
7f6b6eaThanks @ascorbic! - Fixes admin lists, tables and info cards rendering as transparent against the page background. Card containers in the content list, content type list, content type editor, media library, comments, users and device authorization views now have an explicitbg-kumo-basesurface so they're visually distinct from the body.Also fixes column header labels in content list tables ("Title", "Status", etc.) rendering pale because of an undefined Tailwind class (
text-kumo-fg) -- they now use the default text color and rely on the sort indicator icon to signal active state. -
#952
131bea6Thanks @ascorbic! - Replaces 20 raw<input type="checkbox">elements across the admin UI with Kumo'sSwitchandCheckboxcomponents. Single-boolean toggles (SEO, Enable comments, Required, etc.) becomeSwitch; multi-select / list-context checkboxes (collection multi-select, term tree nodes) becomeCheckbox. Drops manual styling and label markup that duplicated what the Kumo components provide built-in. -
#956
54b5aa1Thanks @CacheMeOwside! - Fixes broken checkboxes on the comments moderation page (/_emdash/admin/comments). Selecting a comment threw a JavaScript error and did not select the row. -
#934
c630e31Thanks @ascorbic! - Fixes button and link inconsistencies across the admin UI. Standardises on Kumo'sButtoniconprop andLinkButton(withexternalfor new-tab links) instead of manual icon spacing and raw anchor styling, removes a<Link><Button>invalid HTML nesting in the plugin manager, and translates two stray English strings in the user list empty state. -
#949
7aa1897Thanks @ascorbic! - Fixes invalid<a><button>HTML produced by<Link><Button>...</Button></Link>patterns across the admin UI. Introduces aRouterLinkButtoncomponent that wraps TanStack Router's<Link>with Kumo button styling (variant,size,shape,iconprops), and migrates all existing<Link className={buttonVariants(...)}>usages to use it. Extracts the duplicated "Back to settings" header link into a sharedBackToSettingsLinkcomponent. -
#940
0b8a319Thanks @ascorbic! - Fixes the long tail of untranslated English strings in the admin UI: settings panels, marketplace, sandboxed-plugin host, auth flows, taxonomy/menu management, and lib/api fallback messages. After this PR, EmDash admin UI is fully localizable across all known surfaces. -
#957
13ff061Thanks @ascorbic! - Fixes the OG image picker in the content editor only appearing for collections with a field literally namedfeatured_image. The OG image control now lives in the SEO sidebar panel alongside the other SEO fields, so any collection withseoenabled can set a social preview image regardless of whether it has a featured image field. -
#955
49b66d9Thanks @ascorbic! - Removes the sticky editor header from content / content-type / section / settings pages. The sticky implementation had transparency artifacts (backdrop-blur over varied content), layout fragility (negative margins canceling parent padding), z-index conflicts with the app bar, and ~85px of permanent vertical chrome. Each editor now renders a Save button at the bottom of the form so users can save without scrolling back to the top header. The distraction-free hover-overlay header in the content editor is preserved. -
#966
1b2fa77Thanks @ahliweb! - i18n(id): complete Indonesian translation (320 strings) -
#937
af15975Thanks @ascorbic! - Fixes ~250 untranslated English strings in the admin UI's most-used screens (router toasts, content type editor, widgets, byline and user routes, invite-accept flow, portable text editor toolbar, image and embed editor nodes, user list). Alltitle=,aria-label=,placeholder=, and toast messages in these areas now flow through Lingui. -
#950
a4968c1Thanks @ascorbic! - Replaces raw<select>and<input type="search">elements across the admin UI with Kumo'sSelectandInputcomponents. This gives consistent styling, proper focus rings, accessibility (label association via the Field wrapper), and dark-mode handling for free instead of relying on hand-rolled Tailwind classes that bypassed the design system. -
#973
f80fb58Thanks @ahliweb! - Translates the remaining untranslated string in the Indonesian locale, bringing it to 100% coverage. -
Updated dependencies []:
- @emdash-cms/[email protected]
- If the `credentials` table already exists, manually run `ALTER TABLE credentials ADD COLUMN algorithm INTEGER NOT NULL DEFAULT -7` before upgrading.
- Polymorphic algorithm support for Passkey authentication (adds RS256 alongside ES256)
- Database migration to persist credential algorithms with ALTER TABLE statement
Full changelog
Patch Changes
-
#893
f8ee1edThanks @j-liszt! - Enhances Passkey authentication with polymorphic algorithm support. Adds support for RS256 (RSA) alongside the existing ES256 (ECDSA) implementation, ensuring full compatibility with Windows Hello, hardware security keys, and FIDO2 standards. Includes a database migration to track and persist credential algorithms for future-proof authentication.Note for standalone
@emdash-cms/authconsumers: If yourcredentialstable already exists, you must manually runALTER TABLE credentials ADD COLUMN algorithm INTEGER NOT NULL DEFAULT -7to support this update. TheDEFAULT -7value ensures that existing rows (which are all ES256) continue to work seamlessly without requiring any data backfill.
- Introduces @emdash-cms/plugin-types package providing PluginCapability, CAPABILITY_RENAMES, isDeprecatedCapability, normalizeCapability utilities and manifest shape types (PluginManifest, ManifestHookEntry, ManifestRouteEntry, PluginAdminConfig, PluginStorageConfig) used by emdash core and registry-cli.
Full changelog
Patch Changes
- #923
943df46Thanks @ascorbic! - Adds@emdash-cms/plugin-types: shared TypeScript types for the EmDash plugin manifest contract — capability vocabulary (PluginCapability,CAPABILITY_RENAMES,isDeprecatedCapability,normalizeCapability), manifest shape (PluginManifest,ManifestHookEntry,ManifestRouteEntry,PluginAdminConfig,PluginStorageConfig). Consumed by bothemdash(manifest reader at install/runtime) and@emdash-cms/registry-cli(manifest writer at bundle/publish time). After the registry phase 1 cutover removes the legacy bundling code from core, both sides will continue depending on this single source of truth.
- Deprecated capability aliases (`content:read`, `content:write`, `media:read`, `media:write`, `network:request`, `network:request:unrestricted`) removed; must use current canonical names.
- Plugin descriptor field previously containing a stale hard‑coded literal now emits the actual package version.
Full changelog
Patch Changes
-
#918
1e0cb76Thanks @ascorbic! - Updates declared capabilities to the current names (content:read,content:write,media:read,media:write,network:request,network:request:unrestricted) instead of the deprecated aliases. Plugin descriptors now report the package's own version instead of a stale hard-coded literal. -
Updated dependencies [
a2d3658,c8a3a2c,699e1b3,71f4e7d,7e32092,2e2b8e9,9146931]:
Capability aliases updated to current names and plugin version reporting fixed.
Full changelog
Patch Changes
-
#918
1e0cb76Thanks @ascorbic! - Updates declared capabilities to the current names (content:read,content:write,media:read,media:write,network:request,network:request:unrestricted) instead of the deprecated aliases. Plugin descriptors now report the package's own version instead of a stale hard-coded literal. -
Updated dependencies [
a2d3658,c8a3a2c,699e1b3,71f4e7d,7e32092,2e2b8e9,9146931]:
Fixed interactive project name prompt to accept "." for scaffolding in the current directory.
Full changelog
Patch Changes
- #900
b3d1f40Thanks @mvanhorn! - Fixes interactiveProject name?prompt to accept.for the current directory. The flag-positional path already accepted.(validated byvalidateProjectName), but the prompt's inline regex check rejected it, so users runningnpm create emdash@latestwith no arguments could not scaffold into the current directory. The prompt now usesvalidateProjectNamedirectly for parity, and its message hints at the.option.
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
a2d3658,c8a3a2c,699e1b3,71f4e7d,5eb4318,7e32092,2e2b8e9,9146931]:- [email protected]
- @emdash-cms/[email protected]
- Deprecated capability aliases removed; use `content:read`, `content:write`, `media:read`, `media:write`, `network:request`, `network:request:unrestricted`.
- Plugin descriptor now reports the actual package version instead of a stale hard‑coded literal.
Full changelog
Patch Changes
-
#918
1e0cb76Thanks @ascorbic! - Updates declared capabilities to the current names (content:read,content:write,media:read,media:write,network:request,network:request:unrestricted) instead of the deprecated aliases. Plugin descriptors now report the package's own version instead of a stale hard-coded literal. -
Updated dependencies [
a2d3658,c8a3a2c,699e1b3,71f4e7d,7e32092,2e2b8e9,9146931]:
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
a2d3658,c8a3a2c,699e1b3,71f4e7d,7e32092,2e2b8e9,9146931]:- [email protected]
- @emdash-cms/[email protected]
- Permanent-delete API refuses to remove live (non‑trashed) rows; previously it could delete such rows.
- Permission changed from `import:execute` to `content:delete_permanent` for the permanent‑delete operation.
Full changelog
- Added Tab block to Block Kit for tabbed panel layouts
Full changelog
- Adds `FileFieldRenderer` to open media picker (mime filter disabled) for any file type
- Introduces `hideUrlInput` prop on `MediaPickerModal` to hide image-specific URL input
Full changelog
Patch Changes
-
#719
2e2b8e9Thanks @ascorbic! - Fixes thefilefield type rendering as a plain text input in the content editor. Adds aFileFieldRendererthat opens the media picker (with mime filter disabled) so any file type can be attached. Also adds ahideUrlInputprop toMediaPickerModalso non-image pickers can hide the image-specific "Insert from URL" input.Aligns the Zod schema and generated TypeScript types for
imageandfilefields with the shape the admin actually stores:provider?,meta?(for both), andpreviewUrl?(for image). Previously these fields were stripped on validation and missing from generated types, so site code could not reliably resolve local media URLs frommeta.storageKey. -
Updated dependencies [
5eb4318]:- @emdash-cms/[email protected]
- Plugin descriptors now report the package's own version
Full changelog
Patch Changes
-
#918
1e0cb76Thanks @ascorbic! - Updates declared capabilities to the current names (content:read,content:write,media:read,media:write,network:request,network:request:unrestricted) instead of the deprecated aliases. Plugin descriptors now report the package's own version instead of a stale hard-coded literal. -
Updated dependencies [
a2d3658,c8a3a2c,699e1b3,71f4e7d,7e32092,2e2b8e9,9146931]:
- Rolling back migration `036_i18n_menus_and_taxonomies` is blocked on multi-locale installs to prevent data loss.
- Single‑locale upgrades are additive; locale defaults to `'en'` when omitted.
- Locale-aware i18n support for menus and taxonomies (categories, tags, custom definitions) with `translation_group` storage
- Runtime helpers (`getMenu`, `getTaxonomyTerms`, etc.) accept optional `{ locale }` argument
- REST API endpoints now accept `?locale=xx` query param; new translation‑specific endpoints added
Full changelog
Minor Changes
-
#916
71f4e7dThanks @Rimander! - Adds i18n support to menus and taxonomies (categories, tags, custom
definitions), mirroring the per-locale model already in place for content.
Each row carrieslocaleandtranslation_group; translations of the
same menu/term/def share atranslation_group._emdash_menu_items.reference_id
andcontent_taxonomies.taxonomy_idare remapped to store the referenced
row's translation_group, so a single association survives content
translations and is resolved against the active locale at runtime.- Runtime helpers (
getMenu,getTaxonomyTerms,getTerm,getEntryTerms,
getAllTermsForEntries, …) accept an optional{ locale }and honour the
i18n fallback chain; when no locale is given they fall back to the
request context anddefaultLocale, matchinggetEmDashCollection/
getEmDashEntry. - REST API: GET endpoints accept
?locale=xx; POST endpoints accept
localeandtranslationOfin their bodies. New endpoints:
GET/POST /_emdash/api/menus/:name/translationsand
GET/POST /_emdash/api/taxonomies/:name/terms/:slug/translations. - Creating a content translation now auto-copies the source's taxonomy
assignments (the pivot is locale-agnostic, so the copied rows apply to
the whole translation group). - MCP:
taxonomy_list,taxonomy_list_terms,taxonomy_create_term,
menu_list,menu_getacceptlocale. New tools:
taxonomy_term_translations,menu_translations. - Admin:
TaxonomyManagerandMenuListsurface aLocaleSwitcherwhen
multiple locales are configured and thread the active locale through
all API calls.TaxonomyManagerexposes a "Translate" action per term
that creates the translation and switches to the new locale.
No breaking changes for new installs or single-locale upgrades — defaults
are additive (locale defaults to'en'when omitted, reproducing pre-i18n
behaviour).⚠️ Rolling back migration
036_i18n_menus_and_taxonomiesis blocked
on multi-locale installs. Dropping thelocalecolumn would collapse
translated rows onto an ambiguous(name, slug)unique key, silently
deleting content. The migration'sdown()now refuses to run when any
row uses a non-default locale and prints the affected table in the
error. If you need to revert, export translations first (or delete
them), then re-run the rollback. Single-locale installs revert cleanly. - Runtime helpers (
-
#902
7e32092Thanks @ascorbic! -emdash plugin initnow prompts for the plugin format (sandboxed or native) when run interactively, and the scaffolded boilerplate matches the canonical patterns from the docs. Both formats now ship adist/build via tsdown, declare a samplestoragecollection, and demonstrate a hook plus an API route. The sandboxed entry uses an explicitly typedContentSaveEvent; the native entry forwards options throughcreatePlugin. The descriptoridis now derived from the slug instead of the full scoped package name, so scoped names like@org/my-pluginproduce a runtime-valid id. Pass--format=sandboxed,--format=native, or--nativeto skip the prompt; non-TTY runs continue to default to sandboxed.
Patch Changes
-
#701
a2d3658Thanks @lsngmin! - Fixes MediaValue.src returning bare media ID instead of a usable URL for local media -
#912
c8a3a2cThanks @lsngmin! - Permanent-delete API now refuses to remove live (non-trashed) rows and uses a content-domaincontent:delete_permanentpermission instead of the unrelatedimport:execute. Existing audience (ADMIN-only) is unchanged. -
#896
699e1b3Thanks @cristianuibar! - Fixes 500 error onGET /_emdash/api/dashboardwhen running on Cloudflare D1 with many title-bearing collections.fetchRecentItemsnow issues one query per collection in parallel and merges results in JS instead of building a single chainedUNION ALL, which trips D1'sSQLITE_LIMIT_COMPOUND_SELECTcap once enough collections are present (#895). -
#719
2e2b8e9Thanks @ascorbic! - Fixes thefilefield type rendering as a plain text input in the content editor. Adds aFileFieldRendererthat opens the media picker (with mime filter disabled) so any file type can be attached. Also adds ahideUrlInputprop toMediaPickerModalso non-image pickers can hide the image-specific "Insert from URL" input.Aligns the Zod schema and generated TypeScript types for
imageandfilefields with the shape the admin actually stores:provider?,meta?(for both), andpreviewUrl?(for image). Previously these fields were stripped on validation and missing from generated types, so site code could not reliably resolve local media URLs frommeta.storageKey. -
#911
9146931Thanks @masonjames! - Fixes WordPress media URL rewriting for imported image URLs that use generated size suffixes. -
Updated dependencies [
c8a3a2c,2e2b8e9]:- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
Minor fixes and improvements.
Full changelog
Minor Changes
- #814
a838000Thanks @arashackdev! - rtl srtyle improvements and LTR/RTL compatible arrow/caret icons
Patch Changes
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
e2b3c6c,7b8d496,9dfc65c,e0dc6fb,c22fb3a,6a4e9b8,0ee372a,22a16ee,1e2b024,81662e9,2f22f57,ef3f076,a9c29ea,e7df21f,d5f7c48,8ae227c,e2d5d16,0d98c62,64bf5b9,e81aa0f,a838000,0041d76,cee403d,a8bac5d,5b6f059,a86ff80,d4be24f,eb6dbd0]:- [email protected]
- @emdash-cms/[email protected]
- RTL style improvements
- LTR/RTL compatible arrow and caret icons
Full changelog
Minor Changes
- #814
a838000Thanks @arashackdev! - rtl srtyle improvements and LTR/RTL compatible arrow/caret icons
Patch Changes
- Removed `emdash` as a runtime `dependency`; now declared as a `peerDependency`.
Full changelog
Patch Changes
-
#856
ef3f076Thanks @ask-bonk! - Republishes withemdashas apeerDependencyinstead of a runtimedependency. The previously published0.1.1release pinnedemdashto an ancient0.1.1as a hard dependency, which madenpm installof any template that included this plugin (e.g. the blog template) install two copies ofemdashside-by-side, or fail outright with ERESOLVE on stricter npm configurations (#819). The package source already declared the dependency correctly; this version simply ships the correctedpackage.json. -
Updated dependencies [
e2b3c6c,9dfc65c,e0dc6fb,c22fb3a,6a4e9b8,0ee372a,22a16ee,1e2b024,81662e9,2f22f57,ef3f076,a9c29ea,e7df21f,d5f7c48,8ae227c,e2d5d16,0d98c62,64bf5b9,e81aa0f,0041d76,cee403d,a8bac5d,5b6f059,a86ff80,d4be24f,eb6dbd0]:
Fixes AT Protocol plugin setup by declaring storage collection, normalizing PDS URLs, and exposing missing admin controls.
Full changelog
Patch Changes
-
#734
cf1edaeThanks @huckabarry! - Fixes AT Protocol plugin setup by declaring the storage collection used by the sandbox implementation, normalizing pasted PDS URLs, and exposing the missing site name and publication sync controls in the admin page. -
Updated dependencies [
493e317,3eca9d5,3eca9d5,37ada52,0557b62,5a581d9,0ecd3b4,3138432,70924cd,1f0f6f2,3eca9d5,e402890,3eca9d5,f5658f0,3eca9d5,3eca9d5,b6cb2e6,3eca9d5,cf1edae,b352e88,31333dc,da3d065,3eca9d5,3eca9d5,3eca9d5,47978b5,3eca9d5]:
Minor fixes and improvements.
Full changelog
Minor Changes
- #814
a838000Thanks @arashackdev! - rtl srtyle improvements and LTR/RTL compatible arrow/caret icons
Patch Changes
- Removed the "Blank" template from `npm create emdash` picker; use `starter` for minimal site.
- _key field in Portable Text blocks is now optional in autosave validator.
Full changelog
Minor Changes
-
#859
3015280Thanks @ask-bonk! - Adds non-interactive mode tocreate-emdashfor CI / scripted scaffolding (#711). Pass--template,--platform,--pm,--install/--no-install,--yes, and--forceto skip prompts; partial flag use only prompts for unset fields. Interactive flow is unchanged when no flags are supplied.--template <key>accepts a bare template (blog | starter | marketing | portfolio) or the combined form<platform>:<key>(e.g.cloudflare:blog).--pm <key>(alias--package-manager) selects the package manager.--yes/-yaccepts defaults for any unset field (cloudflare, blog, detected pm,my-sitefor an unset name).--forceis required alongside--yesto overwrite a non-empty target directory; without it, the CLI refuses rather than silently clobbering files.--help/-hprints usage. Unknown flags fail loudly so typos don't silently drop into interactive mode.- An extra positional argument (e.g.
npm create emdash my blogwith a space instead of a hyphen) is now rejected as a likely typo.
No new dependencies — built on
node:util'sparseArgs. -
#811
cee403dThanks @ascorbic! - Scaffolds a freshEMDASH_ENCRYPTION_KEYinto.dev.vars(Cloudflare
templates) or.env(Node templates) on project creation, and ensures the
file is gitignored. Idempotent — won't overwrite an existing key on re-runs.
Patch Changes
-
#852
e73bb5fThanks @ask-bonk! - Removes the "Blank" template from thenpm create emdashpicker. The minimal-content template isstarter; the previously listedblankonly existed for the Node.js path (never Cloudflare) and was confusing. PickStarterfor a minimal site on either platform. -
#869
a8bac5dThanks @ask-bonk! - Fixes autosave validation errors on content seeded from the blog,
portfolio, and starter templates (issue #867).Two related issues:
_keywas strictly required on Portable Text blocks by the
generated Zod schema, but the rest of the block schema is
.passthrough()and the editor regenerates_keyon every change,
so requiring it on input rejected legitimate seed/import data
without protecting any real invariant._keyis now optional in the
validator.- The portfolio template shipped
featured_imageas bare URL strings.
imagefields validate as{ id, ... }objects, so any user who
edited a different field on a portfolio entry hit
featured_image: expected object, received string. The portfolio
seeds now use$mediareferences in the same shape as the blog
template, and every shipped template seed has stable_keys on its
Portable Text nodes.
A regression test runs every shipped template seed through the same
validator the autosave endpoint uses, so future template changes that
break this invariant fail before release.
- Plugins must replace calls to the removed manifest‑related APIs with the new `getManifest()` pattern.
- `emdash auth secret` CLI is deprecated; use the new secrets module (`emdash secrets generate`).
- If you sign preview URLs from a separate process, ensure both processes share the same `EMDASH_PREVIEW_SECRET` or have database access to converge on the auto‑generated value.
- `locals.emdash.invalidateManifest` is removed. Use `locals.emdash.invalidateUrlPatternCache` instead if needed.
- `locals.emdashManifest` is removed. Retrieve the manifest with `await locals.emdash.getManifest()`.
- `EmDashRuntime.invalidateManifest()` is removed. The method no longer exists; use `getManifest()` for access.
- Adds a `media_picker` Block Kit element with thumbnail preview and MIME‑type filter (image types only).
- Introduces an optional `category` field in `PortableTextBlockConfig` to group plugin blocks under custom menu categories.
- `content_publish` now accepts an optional ISO 8601 `publishedAt` for backdating publishes; MCP `content_update` exposes `seo`, `bylines`, and `publishedAt`.
Full changelog
Minor Changes
-
#884
e2b3c6cThanks @ascorbic! - Removes the worker-isolate manifest cache and stops loading the manifest on public requests.The admin manifest (collection schemas, plugins, taxonomies) is built fresh from the live database on every admin request via constant-shape queries (
SchemaRegistry.listCollectionsWithFields()— one collection query plus one batched field query, chunked at the D1 bound-parameter limit; two queries in practice for typical sites), deduplicated within a single request byrequestCached. Logged-out / public requests no longer touch it at all — the global middleware no longer pre-loadslocals.emdashManifest. Admin routes that need it callawait emdash.getManifest().This closes the cross-isolate staleness bug class behind #776, #873, #876, and #877 by elimination: there is no cache to invalidate, so there is nothing to fan out across warm sibling isolates on Cloudflare Workers, and there is nothing to leave stale after a fire-and-forget delete is cancelled at response-time.
Breaking changes
locals.emdash.invalidateManifestis removed. The shim that survived earlier was a misnomer once the manifest cache itself was gone. Plugin code that called this after schema changes should switch tolocals.emdash.invalidateUrlPatternCache(the only side effect that survived) — or drop the call entirely if the mutation didn't affect collection URL patterns (field/taxonomy/plugin mutations don't).locals.emdashManifestis removed. Read it viaawait locals.emdash.getManifest()instead. The only in-tree consumers were the admin manifest endpoint and the WordPress importer routes, both updated.EmDashRuntime.invalidateManifest()is removed.EmDashRuntime.getManifest()is preserved with the same signature; its body now skips the cache layer.
Performance
The admin manifest build is now O(1) query shapes (one for collections, one batched query for the fields of every returned collection, chunked at the D1 bound-parameter limit) instead of N+1. This is the cost the cache was hiding; the rebuild is cheap enough to run per request.
-
#731
9dfc65cThanks @drudge! - Adds amedia_pickerBlock Kit element: a thumbnail preview with a modal library picker and mime-type filter. Usable in plugin block forms and in Block Kit field widgets. The stored value is the selected asset's URL string, so it is value-compatible with a plaintext_input— existing content continues to work after swapping. Themime_type_filteris restricted to image MIME types (image/orimage/<subtype>); wildcards and non-image types are rejected. -
#809
e7df21fThanks @ascorbic! - Adds an optionalcategoryfield toPortableTextBlockConfigfor plugin-contributed block types. Plugins can now choose how their blocks are grouped in the admin slash menu (e.g. "Sections", "Marketing", "Media", "Layout") instead of always falling under "Embeds". Existing plugins that omit the field continue to render under "Embeds" exactly as before. -
#890
8ae227cThanks @ascorbic! - AddspublishedAttocontent_publish(MCP and REST) and exposesseo,bylines, andpublishedAton the MCPcontent_updatetool.content_publishnow accepts an optional ISO 8601publishedAtto backdate a publish, which is useful when migrating content from another CMS or correcting a historical publish date. The override requires thecontent:publish_anypermission. Without it, the existingpublished_atis preserved on re-publish (idempotent) and falls back to the current time on first publish.The MCP
content_updatetool previously droppedseo,bylines, andpublishedAteven though the underlying handler accepted them. Callers had to fall back to raw SQL against_emdash_seoand_emdash_content_bylinesto set these fields. They now flow through the MCP tool and are persisted in the same transaction as field updates. SettingpublishedAtrequirescontent:publish_any, mirroring the REST PUT route. Closes #621 and #622. -
#800
e2d5d16Thanks @csfalcao! - Adds support for accepting passkey assertions from multiple origins that share anrpId, for deployments reachable under several hostnames (apex + preview/staging) under one registrable parent. Declare additional origins viaEmDashConfig.allowedOrigins(inastro.config.mjs) or theEMDASH_ALLOWED_ORIGINSenv var (comma-separated); the two sources merge at runtime. EmDash validates the merged set againstsiteUrland rejects dead config (non-subdomain entries, IP-literalsiteUrl, trailing dots, empty labels) with source-attributed errors.PasskeyConfig.origin: stringis replaced byPasskeyConfig.origins: string[]. -
#837
e81aa0fThanks @netogregorio! - Make the preview URL pattern locale-aware.getPreviewUrl()now accepts a{locale}placeholder and alocaleoption (empty string collapses adjacent slashes so default-locale entries onprefixDefaultLocale: falsesites stay unprefixed). ThePOST /_emdash/api/content/{collection}/{id}/preview-urlroute resolves the locale automatically from the entry and the site's i18n config, and reads a project-wide default pattern from the newEMDASH_PREVIEW_PATH_PATTERNenv var so the admin's "View on site" link can match locale-prefixed routes (e.g./{locale}/{id}). -
#811
cee403dThanks @ascorbic! - Adds a centralized secrets module andemdash secretsCLI command group.
The preview HMAC secret and commenter-IP hash salt are now generated and
persisted in the options table on first need, withEMDASH_PREVIEW_SECRET
andEMDASH_IP_SALTas optional env overrides. This replaces the previous
empty-string preview fallback (which silently disabled token verification)
and the hardcoded"emdash-ip-salt"constant (which was correlatable
across installs).Adds:
emdash secrets generate [--write <file> [--force]]— emits a fresh
EMDASH_ENCRYPTION_KEY(versionedemdash_enc_v1_<43 chars>format),
optionally writes it to.dev.varsor.envidempotently.emdash secrets fingerprint <key>— prints the kid (8-char fingerprint)
for a key without exposing its value.
Lays groundwork for plugin-secret encryption-at-rest in a follow-up.
Deprecates:
emdash auth secret— kept as a working alias that prints a stderr
deprecation note. Will be removed in a future minor.EMDASH_AUTH_SECRET
itself is now legacy: it's only consulted as a fallback IP-salt source
for upgrade compatibility (so existing installs keep stable
commenter-IP hashes). New installs don't need to set it.
API changes:
fingerprintKey()(exported fromemdash's config module) now
validates its input and throwsEmDashSecretsErrorfor malformed or
non-canonical keys, where it previously silently hashed any string.
Callers that want the previous "fingerprint anything" behavior should
hash the input themselves withcrypto.subtle.digest.
User-visible side effects on upgrade:
- Installs that hadn't set
EMDASH_PREVIEW_SECRETget a fresh random
preview secret on first start, which invalidates any outstanding
preview URLs (typically short-lived). - Installs that hadn't set
EMDASH_AUTH_SECRETget a fresh random IP
salt, resetting active comment rate-limit windows once. - Installs that did set
EMDASH_AUTH_SECRETkeep the same IP salt via a
legacy fallback, so existing rate-limit data carries over. - If you sign preview URLs from a separate process without access to the
EmDash database (e.g. a remote preview Worker), you must continue to
setEMDASH_PREVIEW_SECRETin both processes. Processes that share
the database converge on the same auto-generated value automatically;
the env override is only needed when the verifying process can't read
the options table.
-
#816
d4be24fThanks @ask-bonk! - Unifies plugin capability names under a single<resource>[.<sub-resource>]:<verb>[:<qualifier>]formula so capabilities read like RBAC permissions, separates hook-registration permissions from data-access ones for clearer audits, and replaces the overloaded:anyqualifier with the more conspicuous:unrestricted. Old names are still accepted with@deprecatedwarnings;emdash plugin bundleandemdash plugin validatewarn for each deprecated name andemdash plugin publishrefuses manifests that still use them.The Cloudflare sandbox bridge and HTTP fetch helper now enforce canonical names (
content:read,content:write,media:read,media:write,users:read,network:request,network:request:unrestricted). Manifests that still declare legacy names continue to work — the runner normalizes capabilities before passing them into the bridge, so installed plugins withread:contentresolve tocontent:readand reach the same code path.| Old | New |
| ------------------- | -------------------------------- |
|read:content|content:read|
|write:content|content:write|
|read:media|media:read|
|write:media|media:write|
|read:users|users:read|
|network:fetch|network:request|
|network:fetch:any|network:request:unrestricted|
|email:provide|hooks.email-transport:register|
|email:intercept|hooks.email-events:register|
|page:inject|hooks.page-fragments:register|Existing installs keep working — manifests are normalized at every external boundary and
diffCapabilitiesnormalizes both sides so version upgrades that only rename do not trigger a "capability changed" prompt. Deprecated names will be removed in the next minor.
Patch Changes
-
#858
e0dc6fbThanks @ask-bonk! - Adds CSS custom-property hooks to portable-text block defaults inImage,Embed,Gallery, andBreakso host sites can theme figcaptions and horizontal rules without overriding component CSS. Resolution order is--emdash-caption-color→--color-muted→#666for captions,--emdash-break-color→--color-border→#e0e0e0for the break line, and--emdash-break-dots-color→--color-muted→#999for break dots. Backward compatible: sites that don't define any of these variables get the previous hex defaults; sites that already expose the conventional--color-muted/--color-bordertokens (e.g. the blog template) now get correct dark-mode theming automatically. -
#838
c22fb3aThanks @ascorbic! - Removes a redundantSELECT id, author_idlookup that fired after every collection-list and entry fetch when computing the byline-fallback for entries without explicit credits. The column is already on the row data, so it is now read directly. Saves up to one round-trip per list query and two on post-detail routes (~30 fewer queries across the perf-fixture suite). -
#805
6a4e9b8Thanks @ascorbic! - Fixes data loss in the visual-editing inline editor for plugin-contributed Portable Text block types. Previously, custom blocks likemarketing.herolost every field exceptidwhen the page was opened in edit mode, and the next save persisted the loss. Blocks now round-trip losslessly and render as a read-only placeholder labelled with the block type. -
#702
0ee372aThanks @ilicfilip! - Adds@emdash-cms/plugin-field-kit— composable field widgets forjsonfields. Four widgets (object-form,list,grid,tags) are configured entirely through seedoptionsso site builders don't need to write React to get a usable editing UI. Widgets store clean JSON (no nesting, no mutation of shape), so removing the plugin leaves valid data in the database. See discussion #571 for background.Widens
FieldDescriptor.optionstoArray<{ value: string; label: string }> | Record<string, unknown>so plugin widgets can accept arbitrary widget config (not only enum choices). The array shape forselect/multiSelectcontinues to work unchanged. -
#861
22a16eeThanks @ask-bonk! - Fixes "Cannot find module 'kysely'" at runtime afterastro buildfollowed byastro previewornode dist/server/entry.mjson Node deployments using SQLite or libSQL (#741). The SQLite and libSQL dialect runtime modules used CJSrequire("kysely")andrequire("better-sqlite3"), ostensibly to defer loading at config time — though in practice these modules are only ever loaded at runtime viavirtual:emdash/dialect, so the deferral served no purpose. Vite preserved those literalrequire()calls in the bundled SSR chunks; under pnpm's strictnode_moduleslayout, Node's CJS resolver could not findkysely(a transitive dep ofemdash) from the user'sdist/server/chunks/directory. The dialect modules now use static imports — matching the existingdb/postgres.tsadapter — so Vite resolves the deps correctly at build time. -
#847
1e2b024Thanks @ascorbic! - Fixes site favicon injection so user-configured favicons render on the public site, including SVG favicons in Chromium browsers (#831).EmDashHeadnow emits a<link rel="icon">tag with the correcttypeattribute (e.g.image/svg+xml) sourced from the stored media's MIME type. The bundled templates and demos have been updated to drop their per-template favicon link in favour of the centralized injection; existing user sites that still emit their own<link rel="icon">continue to work because browsers tolerate the duplicate.MediaReferencenow carriesurl,contentType,width, andheightwhen resolved viaresolveMediaReference, so callers can emit correct head tags without a second round-trip to the media table. -
#851
81662e9Thanks @ask-bonk! - Fixes admin branding (logo, siteName, favicon) configured via the integration'sadminoption not being delivered to the React admin SPA. The/_emdash/api/manifestroute now reads admin branding from the per-request config plumbed through middleware (the same sourceadmin.astroalready used), instead of a build-time global that was never assigned. -
#857
2f22f57Thanks @ask-bonk! - Fixes a migration race on D1 where two concurrent Workers isolates could both try to apply the same migration, causing one to fail withUNIQUE constraint failed: _emdash_migrations.name. The losing isolate would throw before reaching auto-seed, leaving the manifest cache empty and the admin UI reporting collections as not found while the API reported them correctly.runMigrationsnow treats this specific error as benign, waits for the concurrent migrator to finish, and verifies the schema is fully migrated before returning success. Closes #762. -
#856
ef3f076Thanks @ask-bonk! - Fixesnpm installpeer dependency conflicts (#819) by removing@tanstack/react-queryand@tanstack/react-routerfrompeerDependencies. These libraries are internal implementation details of the bundled admin UI (@emdash-cms/admin) and consuming Astro apps don't import them directly. Listing them as peers ofemdashwas forcing every npm-based install to install and resolve them at the top level, which produced ERESOLVE errors and bloat. The admin package continues to declare them as its own runtime dependencies. -
#817
a9c29eaThanks @all3f0r1! - Fixes redirect middleware so 301/302 rules from_emdash_redirectsactually fire for unauthenticated visitors. Previously, the lookup was silently skipped on the public-visitor branch becauselocals.emdash.dbis intentionally omitted there — only logged-in admins, edit-mode sessions and preview tokens ever saw redirects (so WordPress migration 301s, manual rewrites andAuto: slug changerows did nothing for real traffic, andhits/_emdash_404_logstayed at zero). The middleware now falls back togetDb()(ALS-aware) whenlocals.emdash.dbis absent. Resolves #808. -
#874
d5f7c48Thanks @ask-bonk! - FixesEmDashRuntime.invalidateManifest()leaving the persisted manifest cache row stale on Cloudflare Workers. The D1 row delete was a fire-and-forget promise — on Workers, unawaited work is cancelled when the isolate is torn down post-response, sooptions.emdash:manifest_cachewas almost never actually wiped after a schema mutation. Cold-starting isolates downstream then adopted the pre-mutation snapshot and servedCollection '<slug>' not founduntil something else cleared the row. The delete now goes throughafter(), which hands it toctx.waitUntilunder workerd. (#873) -
#839
0d98c62Thanks @ascorbic! - Caches thesite:*settings prefix-scan across requests within a worker isolate. Site settings change rarely; reading them once per route was wasted work. Writes viasetSiteSettings()invalidate the cache so other isolates pick up changes within their lifetime. -
#840
64bf5b9Thanks @ascorbic! - Reduces duplicate queries on pages that render multiple taxonomy or "recent posts" widgets.getTaxonomyDef(name)now reuses the full taxonomy-defs list when it has already been loaded in the same request, andgetEmDashCollectionbuckets small limits so a post-detail page asking for 4 posts in the body and 5 in a sidebar widget shares one fetch instead of two. Cuts ~6 queries from the perf-fixture post-detail render. -
#803
0041d76Thanks @mvanhorn! - Fixes migrations 034 and 035 so they can safely re-run when a previous attempt left the schema partially applied without recording it in_emdash_migrations. Resolves the "index already exists" error reported on upgrade from 0.1.1 to 0.6.0+. -
#869
a8bac5dThanks @ask-bonk! - Fixes autosave validation errors on content seeded from the blog,
portfolio, and starter templates (issue #867).Two related issues:
_keywas strictly required on Portable Text blocks by the
generated Zod schema, but the rest of the block schema is
.passthrough()and the editor regenerates_keyon every change,
so requiring it on input rejected legitimate seed/import data
without protecting any real invariant._keyis now optional in the
validator.- The portfolio template shipped
featured_imageas bare URL strings.
imagefields validate as{ id, ... }objects, so any user who
edited a different field on a portfolio entry hit
featured_image: expected object, received string. The portfolio
seeds now use$mediareferences in the same shape as the blog
template, and every shipped template seed has stable_keys on its
Portable Text nodes.
A regression test runs every shipped template seed through the same
validator the autosave endpoint uses, so future template changes that
break this invariant fail before release. -
#882
5b6f059Thanks @ascorbic! - Fixes the seed virtual module to also look at the conventionalseed/seed.jsonpath when no.emdash/seed.jsonorpackage.json#emdash.seedpointer is configured. Without this fallback, a site that only hadseed/seed.jsonwould silently fall through to the built-in default seed -- the setup wizard would not offer demo content, and the wrong schema would be applied. The loader now warns when it falls through to the default seed so misconfiguration is loud during dev. -
#855
a86ff80Thanks @ask-bonk! - Fixes Astro session lookups firing on every anonymous public SSR request (#733). The middleware now skipscontext.session.get("user")when noastro-sessioncookie is present, which on Cloudflare Workers (where the Astro session backend is KV) was turning normal anonymous traffic into a flood of KV read misses. Logged-in editors, admin routes, edit/preview flows, and any request that actually carries the session cookie continue to read the session as before. -
#853
eb6dbd0Thanks @drudge! - Fixes content saves on collections with boolean fields. Boolean fields map toINTEGERcolumns and the repository writes booleans as0/1, but never converts them back on read, so a GET → edit → POST round-trip surfaced numbers where the per-collection zod schema expected booleans, and every save was rejected. The boolean field schema now coerces the0/1shape to real booleans at the validation boundary; other numbers and strings still fail validation as before. -
Updated dependencies [
9dfc65c,d6754ae,0ee372a,ef3f076,8d0feb3,8354088,254a443,25128b2,e7df21f,ab45916,0913a39,e2d5d16,a838000,ddbf808,1c958fb,491aeec,d4be24f]:- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- Package for converting Contentful Rich Text documents into Portable Text blocks
Full changelog
Minor Changes
- #699
2e2d4bcThanks @MohamedH1998! - Adds a package for converting Contentful Rich Text documents into Portable Text blocks.
- Update plugin manifests to use new canonical capability names (e.g., `content:read`, `network:request:unrestricted`).
- Existing installs continue working because capabilities are normalized at runtime.
- Deprecation warnings appear in `emdash plugin bundle` and `emdash plugin validate` for each legacy name.
- Old plugin capability names are deprecated and will be removed in the next minor release; manifests using them are rejected by `emdash plugin publish`.
Full changelog
Minor Changes
-
#816
d4be24fThanks @ask-bonk! - Unifies plugin capability names under a single<resource>[.<sub-resource>]:<verb>[:<qualifier>]formula so capabilities read like RBAC permissions, separates hook-registration permissions from data-access ones for clearer audits, and replaces the overloaded:anyqualifier with the more conspicuous:unrestricted. Old names are still accepted with@deprecatedwarnings;emdash plugin bundleandemdash plugin validatewarn for each deprecated name andemdash plugin publishrefuses manifests that still use them.The Cloudflare sandbox bridge and HTTP fetch helper now enforce canonical names (
content:read,content:write,media:read,media:write,users:read,network:request,network:request:unrestricted). Manifests that still declare legacy names continue to work — the runner normalizes capabilities before passing them into the bridge, so installed plugins withread:contentresolve tocontent:readand reach the same code path.| Old | New |
| ------------------- | -------------------------------- |
|read:content|content:read|
|write:content|content:write|
|read:media|media:read|
|write:media|media:write|
|read:users|users:read|
|network:fetch|network:request|
|network:fetch:any|network:request:unrestricted|
|email:provide|hooks.email-transport:register|
|email:intercept|hooks.email-events:register|
|page:inject|hooks.page-fragments:register|Existing installs keep working — manifests are normalized at every external boundary and
diffCapabilitiesnormalizes both sides so version upgrades that only rename do not trigger a "capability changed" prompt. Deprecated names will be removed in the next minor.
Patch Changes
- Adds an `accordion` Block Kit block for collapsible containers with optional default open state.
- Adds a `media_picker` Block Kit element providing thumbnail preview, modal library picker, and MIME-type filtered image selection (stores URL string).
- Improves RTL styling and provides LTR/RTL compatible arrow/caret icons.
Full changelog
Minor Changes
-
#790
7b8d496Thanks @all3f0r1! - Adds anaccordionBlock Kit block: a collapsible container that wraps nested blocks under a labeled trigger. Open/closed state is local to the rendered component (with optionaldefault_open), so plugin admin pages can hide advanced settings, FAQs, or auxiliary panels without paginating or round-tripping throughblock_action. -
#731
9dfc65cThanks @drudge! - Adds amedia_pickerBlock Kit element: a thumbnail preview with a modal library picker and mime-type filter. Usable in plugin block forms and in Block Kit field widgets. The stored value is the selected asset's URL string, so it is value-compatible with a plaintext_input— existing content continues to work after swapping. Themime_type_filteris restricted to image MIME types (image/orimage/<subtype>); wildcards and non-image types are rejected. -
#814
a838000Thanks @arashackdev! - rtl srtyle improvements and LTR/RTL compatible arrow/caret icons
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
e2b3c6c,9dfc65c,e0dc6fb,c22fb3a,6a4e9b8,0ee372a,22a16ee,1e2b024,81662e9,2f22f57,ef3f076,a9c29ea,e7df21f,d5f7c48,8ae227c,e2d5d16,0d98c62,64bf5b9,e81aa0f,0041d76,cee403d,a8bac5d,5b6f059,a86ff80,d4be24f,eb6dbd0]:- [email protected]
- @emdash-cms/[email protected]
- PasskeyConfig.origin: string is replaced by PasskeyConfig.origins: string[]
- `EmDashConfig.allowedOrigins` (in astro.config.mjs) and `EMDASH_ALLOWED_ORIGINS` env var now expect a comma‑separated list of origins which are merged at runtime
- Support for accepting passkey assertions from multiple origins that share an rpId, useful for apex + preview/staging deployments under one registrable parent domain
Full changelog
Minor Changes
- #800
e2d5d16Thanks @csfalcao! - Adds support for accepting passkey assertions from multiple origins that share anrpId, for deployments reachable under several hostnames (apex + preview/staging) under one registrable parent. Declare additional origins viaEmDashConfig.allowedOrigins(inastro.config.mjs) or theEMDASH_ALLOWED_ORIGINSenv var (comma-separated); the two sources merge at runtime. EmDash validates the merged set againstsiteUrland rejects dead config (non-subdomain entries, IP-literalsiteUrl, trailing dots, empty labels) with source-attributed errors.PasskeyConfig.origin: stringis replaced byPasskeyConfig.origins: string[].
- Adds `media_picker` Block Kit element with thumbnail preview, modal library picker and image‑type filter (stores URL string).
- Adds optional `category` field to `PortableTextBlockConfig` for custom grouping in the admin slash menu.
- Introduces consistently‑placed sticky Save buttons across editor pages via shared `EditorHeader` component.
Full changelog
Minor Changes
-
#731
9dfc65cThanks @drudge! - Adds amedia_pickerBlock Kit element: a thumbnail preview with a modal library picker and mime-type filter. Usable in plugin block forms and in Block Kit field widgets. The stored value is the selected asset's URL string, so it is value-compatible with a plaintext_input— existing content continues to work after swapping. Themime_type_filteris restricted to image MIME types (image/orimage/<subtype>); wildcards and non-image types are rejected. -
#809
e7df21fThanks @ascorbic! - Adds an optionalcategoryfield toPortableTextBlockConfigfor plugin-contributed block types. Plugins can now choose how their blocks are grouped in the admin slash menu (e.g. "Sections", "Marketing", "Media", "Layout") instead of always falling under "Embeds". Existing plugins that omit the field continue to render under "Embeds" exactly as before. -
#814
a838000Thanks @arashackdev! - rtl srtyle improvements and LTR/RTL compatible arrow/caret icons -
#854
491aeecThanks @ask-bonk! - Adds consistently-placed sticky Save buttons across editor pages so unsaved changes are always visible. The Content editor, Section editor, Content Type editor, and Settings sub-pages (General, SEO, Social Links) now render their primary save action in a sticky top-right header that stays visible while users scroll long forms. The existing bottom-of-form save buttons are preserved so keyboard and screen-reader users still hit a save action as the last interactive control on the page (DOM order is unchanged). Introduces a sharedEditorHeadercomponent for editor pages that want the same sticky-header pattern. Fixes #233.
Patch Changes
-
#849
d6754aeThanks @drudge! - Fixes thedatetimefield widget so existing values display in the editor and new values pass server validation. The widget passed raw ISO 8601 (YYYY-MM-DDTHH:mm:ss.sssZ) into<input type="datetime-local">, which silently rendered empty, and emittedYYYY-MM-DDTHH:mmon save, which the field's zod schema rejected. Strips the suffix for display, appends:00.000Zon save, and normalizes date-only stored values to UTC midnight for the input. Applies to the top-leveldatetimewidget in the content editor and thedatetimesub-field type insideRepeaterField. -
#702
0ee372aThanks @ilicfilip! - Adds@emdash-cms/plugin-field-kit— composable field widgets forjsonfields. Four widgets (object-form,list,grid,tags) are configured entirely through seedoptionsso site builders don't need to write React to get a usable editing UI. Widgets store clean JSON (no nesting, no mutation of shape), so removing the plugin leaves valid data in the database. See discussion #571 for background.Widens
FieldDescriptor.optionstoArray<{ value: string; label: string }> | Record<string, unknown>so plugin widgets can accept arbitrary widget config (not only enum choices). The array shape forselect/multiSelectcontinues to work unchanged. -
#856
ef3f076Thanks @ask-bonk! - Fixesnpm installpeer dependency conflicts (#819) by removingreactandreact-domfromdependencies. They were declared in bothdependenciesandpeerDependencies, which made npm think the admin package required an exact pinned React version and conflicted with the host Astro app's React. They remainpeerDependencies(^18.0.0 || ^19.0.0), and the host app supplies React. -
#821
8d0feb3Thanks @r2sake! - Fixes the Settings (gear) icon on the Plugin Manager so it links to the plugin's primary admin page instead of a non-existent/settingssub-route. -
#862
8354088Thanks @ask-bonk! - Fixes slug-style<input pattern="...">attributes so HTML form validation works in current browsers. The patterns used[a-z0-9-]+, which is rejected asInvalid character classwhen compiled with thev(unicode-sets) flag — the mode browsers now use for thepatternattribute. The dangling-is now escaped ([a-z0-9\-]+), restoring slug validation in the Sections list/edit, Menus list, and Widgets create-area dialogs. Resolves #845. -
#887
254a443Thanks @ascorbic! - Fixes stale content shown in the Portable Text editor when switching between translations of the same content. Previously, navigating from one locale's editor to another (e.g. from the English version of a post to the French version) kept the previous locale's body in the editor, and any subsequent edit would silently overwrite the new translation's content. The form now resets synchronously when the underlying content item changes, and field editors are keyed by item id so they remount cleanly on a translation switch. -
#885
25128b2Thanks @ahliweb! - Fixes malformed ICU plural syntax in Indonesian (id) locale — ContentList item count now renders correctly -
#872
ab45916Thanks @ahliweb! - Enables Indonesian (Bahasa Indonesia) locale in the admin UI -
#807
0913a39Thanks @ascorbic! - Sizes the plugin block edit modal based on field complexity so Block Kit forms have room to breathe. Simple URL embeds keep the previous compact dialog; forms with several fields get a wider one, and forms containing a repeater open at the largest size. Inputs inside the dialog now fill the available width. -
#815
ddbf808Thanks @ascorbic! - Fixes content list loading state showingNo results for ""instead of a loader while items are being fetched. The trash tab gets the same treatment. -
#870
1c958fbThanks @CacheMeOwside! - Fixes the image-settings icon in the Section editor so it actually opens<ImageDetailPanel>in the sidebar. -
#816
d4be24fThanks @ask-bonk! - Unifies plugin capability names under a single<resource>[.<sub-resource>]:<verb>[:<qualifier>]formula so capabilities read like RBAC permissions, separates hook-registration permissions from data-access ones for clearer audits, and replaces the overloaded:anyqualifier with the more conspicuous:unrestricted. Old names are still accepted with@deprecatedwarnings;emdash plugin bundleandemdash plugin validatewarn for each deprecated name andemdash plugin publishrefuses manifests that still use them.The Cloudflare sandbox bridge and HTTP fetch helper now enforce canonical names (
content:read,content:write,media:read,media:write,users:read,network:request,network:request:unrestricted). Manifests that still declare legacy names continue to work — the runner normalizes capabilities before passing them into the bridge, so installed plugins withread:contentresolve tocontent:readand reach the same code path.| Old | New |
| ------------------- | -------------------------------- |
|read:content|content:read|
|write:content|content:write|
|read:media|media:read|
|write:media|media:write|
|read:users|users:read|
|network:fetch|network:request|
|network:fetch:any|network:request:unrestricted|
|email:provide|hooks.email-transport:register|
|email:intercept|hooks.email-events:register|
|page:inject|hooks.page-fragments:register|Existing installs keep working — manifests are normalized at every external boundary and
diffCapabilitiesnormalizes both sides so version upgrades that only rename do not trigger a "capability changed" prompt. Deprecated names will be removed in the next minor. -
Updated dependencies [
7b8d496,9dfc65c,a838000]:- @emdash-cms/[email protected]
- @emdash-cms/plugin-field-kit added — provides object-form, list, grid, and tags widgets for JSON fields
- FieldDescriptor.options type widened to accept Array or Record
Full changelog
Minor Changes
-
#702
0ee372aThanks @ilicfilip! - Adds@emdash-cms/plugin-field-kit— composable field widgets forjsonfields. Four widgets (object-form,list,grid,tags) are configured entirely through seedoptionsso site builders don't need to write React to get a usable editing UI. Widgets store clean JSON (no nesting, no mutation of shape), so removing the plugin leaves valid data in the database. See discussion #571 for background.Widens
FieldDescriptor.optionstoArray<{ value: string; label: string }> | Record<string, unknown>so plugin widgets can accept arbitrary widget config (not only enum choices). The array shape forselect/multiSelectcontinues to work unchanged.
Patch Changes
- MCP `taxonomy_list_terms` cursor format changed from term-id to base64 keyset; pre-upgrade cursors return `INVALID_CURSOR`
- Repeater Block Kit element for portable-text editor
- Pluggable auth provider system with AT Protocol support
- Settings MCP tools (settings_get, settings_update) with new API token scopes
Full changelog
Minor Changes
-
#679
493e317Thanks @drudge! - Adds arepeaterBlock Kit element: array-of-objects with scalar sub-fields, drag-to-reorder, and collapsible item cards. Plugin block forms can now capture repeating data (FAQ rows, carousel slides, card grids) inline in the portable-text editor. -
#779
e402890Thanks @ascorbic! - Addssettings_getandsettings_updateMCP tools so agents can read and update site-wide settings (title, tagline, logo, favicon, URL, posts-per-page, date format, timezone, social, SEO).settings_getresolves media references (logo/favicon/seo.defaultOgImage) to URLs;settings_updateis a partial update that preserves omitted fields. Newsettings:read(EDITOR+) andsettings:manage(ADMIN) API token scopes back the tools, with matching options in the personal API token settings UI. -
#777
3eca9d5Thanks @ascorbic! - Behavior change — MCPtaxonomy_list_termsnow uses an opaque base64 keyset cursor over(label, id)instead of the previous raw term-id cursor. The new cursor is robust to concurrent term deletion: it encodes a position in sort space rather than a reference to a specific row. MCP clients that persisted page cursors across this upgrade should drop them and restart pagination — pre-upgrade cursors will returnINVALID_CURSOR.Adds parent-chain validation to
taxonomy_create_term(previously onlytaxonomy_update_termvalidated): rejects non-existent parents, cross-taxonomy parents, self-parent on update, cycles on update, and parent chains exceeding 100 ancestors. Existing taxonomies with chains over the depth limit continue to function but cannot accept new descendants until the chain is shortened. -
#675
b6cb2e6Thanks @eyupcanakman! - Renders local media through storagepublicUrlwhen configured.EmDashImageand the Portable Text image block now call a newlocals.emdash.getPublicMediaUrl()helper, so R2 and S3 deployments with a custom domain serve images from that domain.S3Storage.getPublicUrlnow returns the/_emdash/api/media/file/{key}path when nopublicUrlis set (previously{endpoint}/{bucket}/{key}). -
#398
31333dcThanks @simnaut! - Adds pluggable auth provider system with AT Protocol as the first plugin-based provider. Refactors GitHub and Google OAuth from hardcoded buttons into the sameAuthProviderDescriptorinterface. All auth methods (passkey, AT Protocol, GitHub, Google) are equal options on the login page and setup wizard.
Patch Changes
-
#777
3eca9d5Thanks @ascorbic! - Fixes MCP ownership checks failing with an internal error on content that has noauthorId(seed-imported rows). Admins and editors can now edit, publish, unpublish, schedule, and restore such items; users with only own-content permissions get a clean permission error. -
#777
3eca9d5Thanks @ascorbic! - Fixes content create / update silently accepting invalid data: required fields are now enforced, select / multiSelect values must match the configured options, and reference fields must resolve to a real, non-trashed target. Errors surface with a structuredVALIDATION_ERRORcode and a message naming every offending field. -
#670
37ada52Thanks @segmentationfaulter! - Change text direction of input fields and tiptap editor depending upon the language entered -
#688
0557b62Thanks @corwinperdomo! - Fixes the Settings > Email admin page so activeemail:beforeSend/email:afterSendmiddleware plugins are listed (previously always empty). AddsHookPipeline.getHookProviders()for enumerating non-exclusive hook providers. -
#673
5a581d9Thanks @mvanhorn! - Fixes WordPress media import to emit relative/_emdash/api/media/file/...URLs instead of absolute ones, matching every other media endpoint. Imported media is now recognized byINTERNAL_MEDIA_PREFIXfor enrichment, and no longer pins URLs to the origin that happened to serve the import request (breaking renders on a different port or behind a reverse proxy). -
#750
0ecd3b4Thanks @edrpls! - Make the admin collection list column headers sortable.Title,Status,Locale, andDateare now clickable buttons that toggle direction; the current sort state is exposed viaaria-sorton the<th>so screen readers announce it correctly.The server's
orderByfield whitelist now acceptsstatus,locale, andnamealongside the existing date fields — unchanged from a security standpoint, the repo still rejects unknown field names to prevent column enumeration.Callers of
<ContentList>that don't passonSortChangerender the previous static-label headers, so legacy integrations (e.g. the content picker) are unaffected. -
3138432Thanks @r2sake! - Fixes hydration of the inline PortableText editor on pnpm projects by aliasinguse-sync-external-store/shimto the mainuse-sync-external-storepackage. The shim is a CJS-only React<18 polyfill imported transitively by@tiptap/react; under pnpm's virtual store Vite cannot pre-bundle it, and the browser receives rawmodule.exportswhich fails to load as ESM (SyntaxError: ... does not provide an export named 'useSyncExternalStore'). The aliases redirect to React's built-inuseSyncExternalStore(peer-dep floor is React 18), so users no longer need to add the workaround themselves inastro.config.mjs. -
#755
70924cdThanks @mvanhorn! - Fixes the WordPress importer so collections created mid-import are visible to the subsequent execute phase.POST /_emdash/api/import/wordpress/preparenow callsemdash.invalidateManifest()when it creates new collections or fields. Without this, the DB-persisted manifest cache (emdash:manifest_cachein theoptionstable) stays stale and theexecuterequest reportsCollection "<slug>" does not existfor every item destined for a freshly created collection — a bug that survived dev-server restarts and required manually deleting the cache row. -
#757
1f0f6f2Thanks @ascorbic! - Removes two redundant in-scope database queries from the FTS verify-and-repair path. The inner block re-fetched searchable fields and search config that were already loaded in the outer scope of the same method. No behavior change. -
#777
3eca9d5Thanks @ascorbic! - Fixes paginated list endpoints silently returning the first page when given a malformed cursor. Bad cursors now produce a structuredINVALID_CURSORerror so client pagination bugs surface immediately.Note for plugin authors: the low-level
decodeCursorexport fromemdash/database/repositoriesnow throwsInvalidCursorErroron invalid input instead of returningnull. Direct callers (rare — most code usesfindMany-style helpers that handle this internally) should wrap the call intry/catchor migrate to the higher-level helpers. -
#777
3eca9d5Thanks @ascorbic! - Fixesschema_create_collectionMCP tool to apply its documented default of['drafts', 'revisions']forsupportswhen omitted. -
#189
f5658f0Thanks @Sayeem3051! - Add url and email plugin setting field types (Issue #175) -
#777
3eca9d5Thanks @ascorbic! - Preserves structured error codes through MCP tool responses. Errors returned by MCP tools now include a stable[CODE]prefix in the message text and a_meta.codefield on the response envelope, so MCP clients can distinguish failure modes (e.g. NOT_FOUND, CONFLICT, VALIDATION_ERROR) instead of seeing only a generic message. -
#777
3eca9d5Thanks @ascorbic! - Fixesrevision_restorefor collections that support revisions: restore now creates a new draft revision from the source revision's data and updatesdraft_revision_id, leaving the live columns untouched. Previously, restore overwrote the live row directly and left any pending draft unchanged, opposite to the documented contract ("Replaces the current draft..."). The response is also hydrated so the returneddatareflects the post-restore state.Behavior is unchanged for collections that do not support revisions.
-
#734
cf1edaeThanks @huckabarry! - Preserve clearer error logging and run sandboxedafter()content hook tasks in parallel when deferred plugin hooks execute after save and publish. -
#794
b352e88Thanks @ascorbic! - Sanitises thesnippetfield returned by thesearch()API so it is safe to render withset:html/innerHTML. Previously SQLite's FTS5snippet()function spliced literal<mark>tags around matched terms but left the surrounding text unescaped, meaning a post title likeHello <script>alert(1)</script>would render as live markup. Templates and components rendering snippets directly were exposed; the in-treeLiveSearchcomponent already worked around this client-side. Snippets now contain only HTML-escaped source text plus literal<mark>...</mark>highlight tags, matching the documented contract. -
#183
da3d065Thanks @masonjames! - Fixes Astro dev to use the built admin package for external app installs while keeping source aliasing for local monorepo development. -
#777
3eca9d5Thanks @ascorbic! - Tightens conflict-error matchers inhandleContentCreateandhandleContentUpdate. Both paths now match specifically on"unique constraint failed"or"duplicate key"(avoiding false positives where the word "unique" appears in unrelated error text), and produce sanitizedSLUG_CONFLICT/CONFLICTmessages so raw database error text — including Postgres-internal index names — no longer leaks to API consumers. Clients that pattern-match the previous unsanitized messages will see normalized text instead. -
#777
3eca9d5Thanks @ascorbic! - Fixestaxonomy_listexposing collection slugs for collections that no longer exist. Orphaned slugs are filtered out so the response stays consistent withschema_list_collections. -
#777
3eca9d5Thanks @ascorbic! - Fixescontent_unpublishso thatpublishedAtis cleared when an item is unpublished. -
#608
47978b5Thanks @drudge! - Fixes/_emdash/api/widget-areas/*endpoints returning raw DB rows (snake_case fields,contentas a JSON string) instead of the transformedWidgetshape. Admin UI expectscontentto already be a parsed PortableText array andcomponentId/componentProps/menuNamein camelCase, so expanding a content widget in/_emdash/admin/widgetsproduced an empty editor. All four route handlers (GET /widget-areas,GET /widget-areas/:name,POST /widget-areas/:name/widgets,PUT /widget-areas/:name/widgets/:id) now run their results throughrowToWidget, which was made module-exported. -
#777
3eca9d5Thanks @ascorbic! - Addstaxonomies:manageandmenus:manageAPI token scopes for fine-grained control over taxonomy and menu mutations via MCP and REST. Existing tokens withcontent:writecontinue to work for those operations:content:writenow implicitly grantsmenus:manageandtaxonomies:manageso PATs issued before the split keep their effective permissions. The reverse implication does not hold — a token with onlymenus:managecannot create or edit content. -
Updated dependencies [
86b26f6,493e317,e998083,37ada52,acab807,0ecd3b4,4c9f04d,e402890,ed4d880,31333dc,3eca9d5]:- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- Repeater Block Kit element
- Pluggable auth provider system
- Collection list header sorting
Full changelog
Minor Changes
-
#679
493e317Thanks @drudge! - Adds arepeaterBlock Kit element: array-of-objects with scalar sub-fields, drag-to-reorder, and collapsible item cards. Plugin block forms can now capture repeating data (FAQ rows, carousel slides, card grids) inline in the portable-text editor. -
#779
e402890Thanks @ascorbic! - Addssettings_getandsettings_updateMCP tools so agents can read and update site-wide settings (title, tagline, logo, favicon, URL, posts-per-page, date format, timezone, social, SEO).settings_getresolves media references (logo/favicon/seo.defaultOgImage) to URLs;settings_updateis a partial update that preserves omitted fields. Newsettings:read(EDITOR+) andsettings:manage(ADMIN) API token scopes back the tools, with matching options in the personal API token settings UI. -
#398
31333dcThanks @simnaut! - Adds pluggable auth provider system with AT Protocol as the first plugin-based provider. Refactors GitHub and Google OAuth from hardcoded buttons into the sameAuthProviderDescriptorinterface. All auth methods (passkey, AT Protocol, GitHub, Google) are equal options on the login page and setup wizard.
Patch Changes
-
#611
86b26f6Thanks @drudge! - Wires up the block configuration sidebar insideWidgetEditor.PortableTextEditornow receivesonBlockSidebarOpen/onBlockSidebarClosecallbacks that hold the activeBlockSidebarPanelin local state, and rendersImageDetailPanelwhen the panel type is"image"— mirroring the content-entry editor. Without this, clicking a block's settings button or the media picker inside widget content had no visible effect. -
#786
e998083Thanks @smart-cau! - Adds Korean translations for 21 admin UI strings that previously fell back to English. Korean (ko) coverage is now complete. -
#670
37ada52Thanks @segmentationfaulter! - Change text direction of input fields and tiptap editor depending upon the language entered -
#720
acab807Thanks @Pouf5! - Fix taxonomies not nesting correctly in a RTL layout -
#750
0ecd3b4Thanks @edrpls! - Make the admin collection list column headers sortable.Title,Status,Locale, andDateare now clickable buttons that toggle direction; the current sort state is exposed viaaria-sorton the<th>so screen readers announce it correctly.The server's
orderByfield whitelist now acceptsstatus,locale, andnamealongside the existing date fields — unchanged from a security standpoint, the repo still rejects unknown field names to prevent column enumeration.Callers of
<ContentList>that don't passonSortChangerender the previous static-label headers, so legacy integrations (e.g. the content picker) are unaffected. -
#184
4c9f04dThanks @masonjames! - Fixes plugin block defaults so initial values are seeded without overriding later edits. -
#700
ed4d880Thanks @dcardosods! - Prefill site title and tagline in Setup Wizard from seed file -
Updated dependencies [
6e0e921,493e317]:- @emdash-cms/[email protected]
- Repeater Block Kit element with drag-to-reorder
- Empty Block Kit block for empty states
Full changelog
Minor Changes
-
#792
6e0e921Thanks @all3f0r1! - Adds anemptyBlock Kit block: a styled empty-state placeholder with title, optional description, copyable shell command, size variant (sm/base/lg), and an optional list of action elements (CTAs). Plugin admin pages can now render proper empty states for lists, tables, and onboarding flows without rolling their own layout. -
#679
493e317Thanks @drudge! - Adds arepeaterBlock Kit element: array-of-objects with scalar sub-fields, drag-to-reorder, and collapsible item cards. Plugin block forms can now capture repeating data (FAQ rows, carousel slides, card grids) inline in the portable-text editor.
- Support for positional directory argument: `npm create emdash .` scaffolds in current dir; `npm create emdash my-project` skips interactive name prompt
Full changelog
- Pluggable auth provider system with AT Protocol
- settings_get/settings_update MCP tools for site configuration
- Fine-grained API token scopes
Full changelog
Minor Changes
- #779
e402890Thanks @ascorbic! - Addssettings_getandsettings_updateMCP tools so agents can read and update site-wide settings (title, tagline, logo, favicon, URL, posts-per-page, date format, timezone, social, SEO).settings_getresolves media references (logo/favicon/seo.defaultOgImage) to URLs;settings_updateis a partial update that preserves omitted fields. Newsettings:read(EDITOR+) andsettings:manage(ADMIN) API token scopes back the tools, with matching options in the personal API token settings UI.
Patch Changes
-
#398
31333dcThanks @simnaut! - Adds pluggable auth provider system with AT Protocol as the first plugin-based provider. Refactors GitHub and Google OAuth from hardcoded buttons into the sameAuthProviderDescriptorinterface. All auth methods (passkey, AT Protocol, GitHub, Google) are equal options on the login page and setup wizard. -
#777
3eca9d5Thanks @ascorbic! - Addstaxonomies:manageandmenus:manageAPI token scopes for fine-grained control over taxonomy and menu mutations via MCP and REST. Existing tokens withcontent:writecontinue to work for those operations:content:writenow implicitly grantsmenus:manageandtaxonomies:manageso PATs issued before the split keep their effective permissions. The reverse implication does not hold — a token with onlymenus:managecannot create or edit content.
- AT Protocol pluggable auth provider
Full changelog
Minor Changes
- #398
31333dcThanks @simnaut! - Adds pluggable auth provider system with AT Protocol as the first plugin-based provider. Refactors GitHub and Google OAuth from hardcoded buttons into the sameAuthProviderDescriptorinterface. All auth methods (passkey, AT Protocol, GitHub, Google) are equal options on the login page and setup wizard.
Patch Changes
- Updated dependencies [
493e317,3eca9d5,3eca9d5,37ada52,0557b62,5a581d9,0ecd3b4,3138432,70924cd,1f0f6f2,3eca9d5,e402890,3eca9d5,f5658f0,3eca9d5,3eca9d5,b6cb2e6,3eca9d5,cf1edae,b352e88,31333dc,da3d065,3eca9d5,3eca9d5,3eca9d5,47978b5,3eca9d5]:- [email protected]
- @emdash-cms/[email protected]
- Restricts Subscriber-role access to draft, scheduled, and trashed content
- New content:read_drafts permission for Contributor+ roles
Full changelog
Patch Changes
- #736
81fe93bThanks @ascorbic! - Restricts Subscriber-role access to draft, scheduled, and trashed content. Subscribers retaincontent:readfor member-only published content but no longer see non-published items via the REST API or MCP server. Adds a newcontent:read_draftspermission (Contributor and above) that gates/compare,/revisions,/trash,/preview-url, and the corresponding MCP tools.
- DoS vulnerability in 404 log with unbounded row growth
- Setup window admin hijack prevention via nonce binding
- SSRF protection against DNS-rebinding attacks
- Admin white-labeling via config
- trustedProxyHeaders for reverse proxy rate limiting
Full changelog
Minor Changes
-
#705
8ebdf1aThanks @eba8! - Adds admin white-labeling support viaadminconfig inastro.config.mjs. Agencies can set a custom logo, site name, and favicon for the admin panel, separate from public site settings. -
#742
c26442bThanks @ascorbic! - AddstrustedProxyHeadersconfig option so self-hosted deployments behind a reverse proxy can declare which client-IP headers to trust. Used by auth rate limits (magic-link, signup, passkey, OAuth device flow) and the public comment endpoint — without it, every request on a non-Cloudflare deployment was treated as "unknown" and rate limits were effectively disabled.Set the option in
astro.config.mjs:emdash({ trustedProxyHeaders: ["x-real-ip"], // nginx, Caddy, Traefik });or via the
EMDASH_TRUSTED_PROXY_HEADERSenv var (comma-separated). Headers are tried in order; values ending inforwarded-forare parsed as comma-separated lists.Also removes the user-agent-hash fallback on the comment endpoint. The fallback was meant to give anonymous commenters on non-Cloudflare deployments something approximating per-user rate limiting, but the UA is trivially rotatable; requests with no trusted IP now share a stricter "unknown" bucket. Operators behind a reverse proxy should set
trustedProxyHeadersto restore per-IP bucketing.Only set
trustedProxyHeaderswhen you control the reverse proxy. Trusting a forwarded-IP header from the open internet lets any client spoof their IP and defeats rate limiting.
Patch Changes
-
#745
7186961Thanks @ascorbic! - Fixes an unauthenticated denial-of-service via the 404 log. Every 404 response previously inserted a new row into_emdash_404_log, so an attacker could grow the database without bound by requesting unique nonexistent URLs. Repeat hits to the same path now dedup into a single row with ahitscounter andlast_seen_attimestamp, referrer and user-agent headers are truncated to bounded lengths, and the log is capped at 10,000 rows with oldest-first eviction. -
#739
e9ecec2Thanks @MohamedH1998! - Fixes the REST content API silently strippingpublishedAton create/update andcreatedAton create. Importers can now preserve original publish and creation dates on migrated content. Gated behindcontent:publish_any(EDITOR+) so regular contributors cannot backdate posts.createdAtis intentionally not accepted on update —created_atis treated as immutable. -
#732
e3e18aaThanks @jcheese1! - Fixes select dropdown appearing behind dialog by removing explicit z-index values and addingisolateto the admin body for proper stacking context. -
#695
fae63bdThanks @ascorbic! - Fixesemdash seedso entries declared with"status": "published"are actually published. Previously the seed wrote the content row withstatus: "published"and apublished_attimestamp but never created a live revision, so the admin UI showed "Save & Publish" instead of "Unpublish" andlive_revision_idstayed null. The seed now promotes published entries to a live revision on both create and update paths. -
#744
30d8fe0Thanks @ascorbic! - Fixes a setup-window admin hijack by binding/setup/adminand/setup/admin/verifyto a per-session nonce cookie. Previously an unauthenticated attacker who could reach a site during first-time setup could POST to/setup/adminbetween the legitimate admin's email submission and passkey verification, overwriting the stored email — the admin account would then be created with the attacker's address. The admin route now mints a cryptographically random nonce, stores it in setup state, and sets it as an HttpOnly, SameSite=Strict,/_emdash/-scoped cookie; the verify route rejects any request whose cookie does not match in constant time. -
#685
d4a95bfThanks @ascorbic! - Fixes visual editing: clicking an editable field now opens the inline editor instead of always opening the admin in a new tab. The toolbar's manifest fetch was readingmanifest.collectionsdirectly but the/_emdash/api/manifestendpoint wraps its payload in{ data: … }, so every field-kind lookup returnednulland every click fell through to the admin-new-tab fallback. -
#743
a31db7dThanks @ascorbic! - Locksemdash:site_urlafter the first setup call so a spoofed Host header on a later step of the wizard can't overwrite it. Config (siteUrl) and env (EMDASH_SITE_URL) paths already took precedence; this is a defence-in-depth guard for deployments that rely on the request-origin fallback. -
#737
adb118cThanks @ascorbic! - Rate-limits the self-signup request endpoint to prevent abuse.POST /_emdash/api/auth/signup/requestnow allows 3 requests per 5 minutes per IP, matching the existing limit on magic-link/send. Over-limit requests return the same generic success response as allowed-but-ignored requests, so the limit isn't observable to callers. -
#738
080a4f1Thanks @ascorbic! - Strengthens SSRF protection on the import pipeline against DNS-rebinding. ThevalidateExternalUrlhelper now also blocks known wildcard DNS services (nip.io,sslip.io,xip.io,traefik.me,lvh.me,localtest.me) and trailing-dot FQDN forms of blocked hostnames. A newresolveAndValidateExternalUrlresolves the target hostname via DNS-over-HTTPS (Cloudflare) and rejects if any returned IP is in a private range.ssrfSafeFetchand the plugin unrestricted-fetch path now use the DNS-aware validator on every hop. This adds two DoH round-trips per outbound request; self-hosted admins whose egress blockscloudflare-dns.comcan inject a custom resolver viasetDefaultDnsResolver. -
#736
81fe93bThanks @ascorbic! - Restricts Subscriber-role access to draft, scheduled, and trashed content. Subscribers retaincontent:readfor member-only published content but no longer see non-published items via the REST API or MCP server. Adds a newcontent:read_draftspermission (Contributor and above) that gates/compare,/revisions,/trash,/preview-url, and the corresponding MCP tools. -
Updated dependencies [
8ebdf1a,2e4b205,e3e18aa,743b080,fa8d753,81fe93b]:- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- Plugin HTTP requests re-validate destinations at each redirect hop
- Credential headers stripped on cross-origin redirects
- Private IPs rejected even with allowedHosts wildcard
Full changelog
Patch Changes
-
#740
63509e1Thanks @ascorbic! - Sandboxed plugin HTTP requests now follow redirects manually and re-validate the destination at every hop. The allowedHosts list is checked on each redirect target (not just the initial URL), so an allowed host that 302s to a disallowed one no longer bypasses the scope. Credential headers (Authorization, Cookie, Proxy-Authorization) are stripped on cross-origin redirects.network:fetch:anyandallowedHosts: ["*"]now still reject literal private IPs, cloud-metadata addresses, and known internal hostnames — the allowlist scopes which public hosts a plugin may reach, not whether SSRF protection applies. Non-http(s) URL schemes are rejected. Caps redirect chains at 5 hops. -
Updated dependencies [
8ebdf1a,7186961,e9ecec2,e3e18aa,fae63bd,30d8fe0,d4a95bf,a31db7d,adb118c,080a4f1,81fe93b,c26442b]:
- Admin white-labeling with custom logo and site name
- Persian locale with full translations
- Taxonomy term picker diacritic-insensitive matching
Full changelog
Minor Changes
- #705
8ebdf1aThanks @eba8! - Adds admin white-labeling support viaadminconfig inastro.config.mjs. Agencies can set a custom logo, site name, and favicon for the admin panel, separate from public site settings.
Patch Changes
-
#680
2e4b205Thanks @CacheMeOwside! - Fixes dark mode toggle having no effect with the classic theme. -
#732
e3e18aaThanks @jcheese1! - Fixes select dropdown appearing behind dialog by removing explicit z-index values and addingisolateto the admin body for proper stacking context. -
#647
743b080Thanks @arashackdev! - Adds Persian (Farsi) locale with full admin translations.
Adds Vazirmatn as the default font family for Farsi. -
#689
fa8d753Thanks @edrpls! - Fixes the taxonomy term picker to match across diacritic boundaries.Typing
Mexicoin the admin picker now surfaces a term labeledMéxicoinstead of prompting a duplicate create. Input and term labels are folded via NFD decomposition + lowercase before substring-matching, so editors who type without diacritics — or with locale keyboards that produce precomposed vs. combining forms — still see the canonical term.Before this fix,
"mexico"and"méxico"were treated as distinct strings, so the picker showed zero suggestions and the editor had no way to find the existing term except to create a duplicate. Duplicate terms then split the taxonomy and broke public-facing filter pages that group content by slug.The exact-match check that gates the "Create new term" button uses the same fold, so typing
MexicowhenMéxicoexists also suppresses Create — closing the duplicate-creation loop. -
Updated dependencies []:
- @emdash-cms/[email protected]
- Reserved field slugs `terms`, `bylines`, `byline` now blocked; existing conflicting fields overwritten on read
- Eager taxonomy term hydration on entry results
- Noto Sans default admin font with script configuration
- Cold-start performance improvements for D1 read replicas
Full changelog
Minor Changes
-
#626
1859347Thanks @ascorbic! - Adds eager hydration of taxonomy terms ongetEmDashCollectionandgetEmDashEntryresults. Each entry now exposes adata.termsfield keyed by taxonomy name (e.g.post.data.terms.tag,post.data.terms.category), populated via a single batched JOIN query alongside byline hydration. Templates that previously looped and calledgetEntryTerms(collection, id, taxonomy)per entry can readentry.data.termsdirectly and skip the N+1 round-trip.New exports:
getAllTermsForEntries,invalidateTermCache.Reserved field slugs now also block
terms,bylines, andbylineat schema-creation time to prevent new fields shadowing the hydrated values. Existing installs that already have a user-defined field with any of those slugs will see the hydrated value overwrite the stored value on read (consistent with the pre-existing behavior ofbylines/bylinehydration); rename the field to keep its data accessible. -
#600
9295cc1Thanks @ascorbic! - Adds Noto Sans as the default admin UI font via the Astro Font API. Fonts are downloaded from Google at build time and self-hosted. The base font covers Latin, Cyrillic, Greek, Devanagari, and Vietnamese. Additional scripts (Arabic, CJK, Hebrew, Thai, etc.) can be added via the newfonts.scriptsconfig option. Setfonts: falseto disable and use system fonts.
Patch Changes
-
#648
ada4ac7Thanks @CacheMeOwside! - Adds the missingurlfield type for seed files, content type builder, and content editor with client-side URL validation. -
#658
f279320Thanks @ascorbic! - Addsafter(fn)— a helper for deferring bookkeeping work past the HTTP response. On Cloudflare it hands off towaitUntil(extending the worker's lifetime); on Node it fire-and-forgets (the event loop keeps the process alive for the next request anyway). Host binding is plumbed through a newvirtual:emdash/wait-untilvirtual module so core stays runtime-neutral — Cloudflare-specific imports live in the integration layer, not in request-handling code.First use: cron stale-lock recovery (
_emdash_cron_tasksUPDATE) now runs after the response ships instead of blocking it. On D1 this shaves a primary-routed write off the cold-start critical path.Usage:
import { after } from "emdash"; // Fire-and-forget; errors are caught and logged so a deferred task // never surfaces as an unhandled rejection. after(async () => { await recordAuditEntry(); }); -
#642
7f75193Thanks @Pouf5! - AddsmaxUploadSizeconfig option to set the maximum media file upload size in bytes. Defaults to 52_428_800 (50 MB) — existing behaviour is unchanged. -
#595
cfd01f3Thanks @ascorbic! - Fixes playground initialization crash caused by syncSearchState attempting first-time FTS enablement during field creation. -
#663
38d637bThanks @ascorbic! - CachegetSiteSetting(key)per-request. It was firing an uncachedoptionstable read on every call, so templates that pull several settings (orEmDashHeadreadingseoon every page render) paid N round-trips to the D1 primary instead of sharing one. Noticeable on colos far from the primary — APS/APE were seeing ~30–100 ms of avoidable warm-render latency per page.Wraps each key in
requestCached("siteSetting:${key}", ...)so concurrent callers in a single render share the in-flight query. -
#631
31d2f4eThanks @ascorbic! - Improves cold-start performance for anonymous page requests. Sites with D1 replicas far from the worker colo should see the biggest improvement; on the blog-demo the homepage cold request on Asia colos dropped from several seconds to under a second.Three underlying changes:
- Search index health checks run on demand (on the first search request) rather than at worker boot, reclaiming the time a boot-time scan spent walking every searchable collection.
- Module-scoped caches (manifest, taxonomy names, byline existence, taxonomy-assignment existence) are now reused across anonymous requests that route through D1 read replicas. They previously rebuilt on every request.
- Cold-start Server-Timing headers break runtime init into sub-phases (
rt.db,rt.plugins, etc.) so further regressions are easier to diagnose.
-
#605
445b3bfThanks @ascorbic! - Fixes D1 read replicas being bypassed for anonymous public page traffic. The middleware fast path now asks the database adapter for a per-request scoped Kysely, so anonymous reads land on the nearest replica instead of the primary-pinned singleton binding.All D1-specific semantics (Sessions API, constraint selection, bookmark cookie) live in
@emdash-cms/cloudflare/db/d1behind a singlecreateRequestScopedDb(opts)function. Core middleware has no D1-specific logic. Adapters opt in via a newsupportsRequestScope: booleanflag onDatabaseDescriptor;d1()sets it to true.Other fixes in the same change:
- Nested
runWithContextcalls in the request-context middleware now merge the parent context instead of replacing it, so an outer per-request db override is preserved through edit/preview flows. - Baseline security headers now forward Astro's cookie symbol across the response clone so
cookies.set()calls in middleware survive. - Any write (authenticated or anonymous) now forces
first-primary, so an anonymous form/comment POST isn't racing across replicas. - The session user is read once per request and reused in both the fast path and the full runtime init (previously read twice on authenticated public-page traffic).
- Bookmark cookies are validated only for length (≤1024) and absence of control characters — no stricter shape check, so a future D1 bookmark format change won't silently degrade consistency.
- The
!configbail-out now still applies baseline security headers. __ec_d1_bookmarkreferences aligned to__em_d1_bookmarkacross runtime, docs, and JSDoc.
- Nested
-
#654
943d540Thanks @ascorbic! - Dedups repeat DB queries within a single page render. Measured against the query-count fixture:- The "has any bylines / has any taxonomy terms" probes were module-scoped singletons, but the bundler duplicates those modules across chunks — each chunk ended up with its own copy of the singleton, so the probe re-ran whenever a different chunk called the helper. Stored on
globalThiswith a Symbol key (same pattern asrequest-context.ts), so a single value is shared across all chunks now. - Wraps
getCollectionInfo,getTaxonomyDef,getTaxonomyTerms, andgetEmDashCollectionin the request-scoped cache so two callers with the same arguments in the same render share a single query.
Biggest wins land on pages that render multiple content-heavy components (a post detail page with comments, byline credits, and sidebar widgets). On the fixture post page: -3 queries cold / -1 warm under SQLite, -2 queries cold under D1.
- The "has any bylines / has any taxonomy terms" probes were module-scoped singletons, but the bundler duplicates those modules across chunks — each chunk ended up with its own copy of the singleton, so the probe re-ran whenever a different chunk called the helper. Stored on
-
#668
2cb3165Thanks @CacheMeOwside! - Fixes boolean field checkbox displaying as unchecked after publish in the admin UI. -
#500
14c923bThanks @all3f0r1! - Adds inline term creation in the post editor taxonomy sidebar. Tags show a "Create" option when no match exists; categories get an "Add new" button below the list. -
#606
c5ef0f5Thanks @ascorbic! - Caches the manifest in memory and in the database to eliminate N+1 schema queries per request. Batches site info queries during initialization. Cold starts read 1 cached row instead of rebuilding from scratch. -
#671
f839381Thanks @jcheese1! - Fixes MCP OAuth discovery and dynamic client registration so EmDash only advertises supported client registration mechanisms and rejects unsupported redirect URIs or token endpoint auth methods during client registration. Also exempts OAuth protocol endpoints (token, register, device code, device token) from the Origin-based CSRF check, since these endpoints are called cross-origin by design (MCP clients, CLIs, native apps) and carry no ambient credentials, and sends the required CORS headers so browser-based MCP clients can reach them. -
#664
002d0acThanks @ascorbic! -getSiteSetting(key)now transparently piggybacks ongetSiteSettings()when the batch has already been loaded in the current request. If a parent template has calledgetSiteSettings()(which is request-cached), a latergetSiteSetting("seo")— fromEmDashHead, a plugin, or user code — reads the key from that cached result instead of firing its own round-trip. Falls back to a per-key cached query when nothing has been primed.Exposes
peekRequestCache(key)for internal use by other helpers that want the same "read from a broader cached query if available" pattern.On the blog-demo fixture: the SEO call added in PR #613 now costs zero extra queries per page (it reads from the Base layout's existing
getSiteSettings()result). -
#465
0a61ef4Thanks @Pouf5! - Fixes FTS5 tables not being created when a searchable collection is created or updated via the Admin UI. -
#636
6d41fe1Thanks @ascorbic! - Fixes two correctness issues from the #631 cold-start work:ensureSearchHealthy()now runs against the runtime's singleton database instead of the per-request session-bound one. The verify step reads, but a corrupted index triggers a rebuild write, and D1 Sessions on a GET request usesfirst-unconstrainedrouting that's free to land on a replica. The singleton goes through the default binding, which the adapter correctly promotes tofirst-primaryfor writes.- The playground request-context middleware now sets
dbIsIsolated: true. Without it, schema-derived caches (manifest, taxonomy defs, byline/term existence probes) could carry values across playground sessions that have independent schemas.
-
#627
b158e40Thanks @ascorbic! - Prime the request-scoped cache forgetEntryTermsduring collection and entry hydration.getEmDashCollectionandgetEmDashEntryalready fetch taxonomy terms for their results via a single batched JOIN; now the same data is seeded into the per-request cache under the same keysgetEntryTermsuses, so existing templates that still callgetEntryTerms(collection, id, taxonomy)in a loop get cache hits instead of a serial DB round-trip per iteration.Empty-result entries are seeded with
[]for every taxonomy that applies to the collection so "this post has no tags" also short-circuits without a query. Cache entries are scoped to the request context via ALS and GC'd with it. -
#653
f97d6abThanks @ascorbic! - Adds opt-in query instrumentation for performance regression testing. SettingEMDASH_QUERY_LOG=1causes the Kysely log hook to emit[emdash-query-log]-prefixed NDJSON on stdout for every DB query executed inside a request, tagged with the route, method, and anX-Perf-Phaseheader value. Zero runtime overhead when the flag is unset — the log option is only attached to Kysely when enabled.Also exposes the helpers at
emdash/database/instrumentationso first-party adapters (e.g.@emdash-cms/cloudflare) can wire the same hook into their per-request Kysely instances. -
#613
e67b940Thanks @nickgraynews! - Fixes site SEO settingsgoogleVerificationandbingVerificationnot being emitted into<head>. The fields were stored in the database and editable in the admin UI but were never rendered as<meta name="google-site-verification">or<meta name="msvalidate.01">tags, making meta-tag verification with Google Search Console and Bing Webmaster Tools impossible. EmDashHead now loads site SEO settings and renders these tags on every page. -
#659
0896ec8Thanks @ascorbic! - Two query-count reductions on the request hot path:- Widget areas now fetch in a single query.
getWidgetArea(name)used to do two round-trips — one for the area, one for its widgets. Single left-join now. Saves one query per<WidgetArea>rendered on a page. - Dropped the "has any bylines / has any term assignments" probes. Those fired on every hydration call to save a single query on sites with zero bylines/terms — exactly the wrong tradeoff. The batch hydration queries already handle empty sites at the same cost, so the probes are removed. Pre-migration databases (tables not created yet) are still handled via an
isMissingTableErrorcatch. Saves two queries per render on pages that hydrate bylines and taxonomy terms.
On the fixture post-detail page: SQLite
/posts/[slug]drops from 34 → 32, D1 from 43 → 39. The widget-area JOIN shaves one off every page that renders a widget area.invalidateBylineCache()andinvalidateTermCache()are preserved as no-op exports so callers don't break. - Widget areas now fetch in a single query.
-
#558
629fe1dThanks @csfalcao! - Fixes/_emdash/api/search/suggest500 error.getSuggestionsno longer double-appends the FTS5 prefix operator*on top of the oneescapeQueryalready adds, so autocomplete queries like?q=desnow return results instead of raisingSqliteError: fts5: syntax error near "*". -
#552
f52154dThanks @masonjames! - Fixes passkey login failures so unregistered or invalid credentials return an authentication failure instead of an internal server error. -
#601
8221c2aThanks @CacheMeOwside! - Fixes the Save Changes button on the Content Type editor failing silently with a 400 error -
#598
8fb93ebThanks @maikunari! - Fixes WordPress import error reporting to surface the real exception message instead of a generic "Failed to import item" string, making import failures diagnosable. -
#629
6d7f288Thanks @CacheMeOwside! - Adds toast feedback when taxonomy assignments are saved or fail on content items. -
#638
4ffa141Thanks @auggernaut! - Fixes repeated FTS startup rebuilds on SQLite by verifying indexed row counts against the FTS shadow table. -
#582
04e6ccaThanks @all3f0r1! - Improves the "Failed to create database" error to detect NODE_MODULE_VERSION mismatches from better-sqlite3 and surface an actionable message telling the user to rebuild the native module. -
Updated dependencies [
dfcb0cd,cf63b02,0b32b2f,913cb62,6c92d58,a2d5afb,39d285e,f52154d]:- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
Fixes passkey login failures to return authentication errors instead of internal server errors.
Full changelog
Patch Changes
- #552
f52154dThanks @masonjames! - Fixes passkey login failures so unregistered or invalid credentials return an authentication failure instead of an internal server error.
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
ada4ac7,f279320,7f75193,cfd01f3,38d637b,31d2f4e,445b3bf,943d540,2cb3165,1859347,14c923b,c5ef0f5,f839381,002d0ac,0a61ef4,6d41fe1,b158e40,f97d6ab,e67b940,0896ec8,629fe1d,f52154d,8221c2a,8fb93eb,6d7f288,4ffa141,04e6cca,9295cc1]:- [email protected]
- @emdash-cms/[email protected]
- Opt‑in query instrumentation via `EMDASH_QUERY_LOG=1` emits NDJSON logs for every DB query in a request, tagged with route, method and `X-Perf-Phase`.
- `createRequestScopedDb(opts)` centralizes D1‑specific semantics behind a single function; adapters opt‑in using `supportsRequestScope: true` on `DatabaseDescriptor`.
Full changelog
Patch Changes
-
#605
445b3bfThanks @ascorbic! - Fixes D1 read replicas being bypassed for anonymous public page traffic. The middleware fast path now asks the database adapter for a per-request scoped Kysely, so anonymous reads land on the nearest replica instead of the primary-pinned singleton binding.All D1-specific semantics (Sessions API, constraint selection, bookmark cookie) live in
@emdash-cms/cloudflare/db/d1behind a singlecreateRequestScopedDb(opts)function. Core middleware has no D1-specific logic. Adapters opt in via a newsupportsRequestScope: booleanflag onDatabaseDescriptor;d1()sets it to true.Other fixes in the same change:
- Nested
runWithContextcalls in the request-context middleware now merge the parent context instead of replacing it, so an outer per-request db override is preserved through edit/preview flows. - Baseline security headers now forward Astro's cookie symbol across the response clone so
cookies.set()calls in middleware survive. - Any write (authenticated or anonymous) now forces
first-primary, so an anonymous form/comment POST isn't racing across replicas. - The session user is read once per request and reused in both the fast path and the full runtime init (previously read twice on authenticated public-page traffic).
- Bookmark cookies are validated only for length (≤1024) and absence of control characters — no stricter shape check, so a future D1 bookmark format change won't silently degrade consistency.
- The
!configbail-out now still applies baseline security headers. __ec_d1_bookmarkreferences aligned to__em_d1_bookmarkacross runtime, docs, and JSDoc.
- Nested
-
#569
134f776Thanks @Yusaku01! - Fixes the playground toolbar layout on small screens. -
#653
f97d6abThanks @ascorbic! - Adds opt-in query instrumentation for performance regression testing. SettingEMDASH_QUERY_LOG=1causes the Kysely log hook to emit[emdash-query-log]-prefixed NDJSON on stdout for every DB query executed inside a request, tagged with the route, method, and anX-Perf-Phaseheader value. Zero runtime overhead when the flag is unset — the log option is only attached to Kysely when enabled.Also exposes the helpers at
emdash/database/instrumentationso first-party adapters (e.g.@emdash-cms/cloudflare) can wire the same hook into their per-request Kysely instances. -
Updated dependencies [
ada4ac7,f279320,7f75193,cfd01f3,38d637b,31d2f4e,445b3bf,943d540,2cb3165,1859347,14c923b,c5ef0f5,f839381,002d0ac,0a61ef4,6d41fe1,b158e40,f97d6ab,e67b940,0896ec8,629fe1d,f52154d,8221c2a,8fb93eb,6d7f288,4ffa141,04e6cca,9295cc1]:
- Full RTL (right‑to‑left) layout for admin UI via direction‑aware Tailwind classes
- Korean locale support added to admin UI
- Chinese (Traditional) translation added for login, settings and locale switcher
Full changelog
Minor Changes
- #565
913cb62Thanks @ophirbucai! - Adds full RTL (right-to-left) support to the admin UI by converting all directional Tailwind classes to their direction-aware equivalents.
Patch Changes
-
#610
dfcb0cdThanks @drudge! - Passes plugin block definitions into thePortableTextEditornested insideWidgetEditor, so custom plugin-registered block types (image blocks, marker blocks, etc.) can be inserted and rendered inside content-type widgets. The manifest is fetched with react-query in the top-levelWidgetscomponent, flattened into aPluginBlockDef[]list, and threaded throughWidgetAreaPanel→WidgetItem→WidgetEditor. -
#568
cf63b02Thanks @Vallhalen! - Fix document outline not showing headings on initial load. The outline now defers initial extraction to next tick (so TipTap finishes hydrating) and also listens for transaction events to catch programmatic content changes. -
#564
0b32b2fThanks @ascorbic! - Replaces the horizontal language-switch button bar on the admin login page with a dropdown, so the selector stays usable as more locales are added. -
#592
6c92d58Thanks @asdfgl98! - Adds Korean locale support to the admin UI. -
#559
a2d5afbThanks @ayfl269! - Adds Chinese (Traditional) translation for the admin UI, including login page, settings page, and locale switching. -
#604
39d285eThanks @all3f0r1! - Fixes loading spinner not centered under logo on the login page. -
Updated dependencies []:
- @emdash-cms/[email protected]
- Adds `where: { status?, locale? }` to `ContentListOptions` for database‑layer filtering
- Introduces RTL language support with LocaleDirectionProvider component
Full changelog
Minor Changes
-
#540
82c6345Thanks @jdevalk! - Addswhere: { status?, locale? }toContentListOptions, letting plugins narrowContentAccess.list()results at the database layer instead of filtering the returned array. The underlying repository already supports these filters — this PR only exposes them through the plugin-facing type. -
#551
598026cThanks @ophirbucai! - Adds RTL (right-to-left) language support infrastructure. Enables proper text direction for RTL languages like Arabic, Hebrew, Farsi, and Urdu. Includes LocaleDirectionProvider component that syncs HTML dir/lang attributes with Kumo's DirectionProvider for automatic layout mirroring when locale changes.
Patch Changes
-
#542
64f90d1Thanks @mohamedmostafa58! - Fixes invite flow: corrects invite URL to point to admin UI page, adds InviteAcceptPage for passkey registration. -
#555
197bc1bThanks @ascorbic! - Fixes OAuth authorization server metadata discovery for MCP clients by serving it at the RFC 8414-compliant path. -
#534
ce873f8Thanks @ttmx! - Fixes Table block to render inline marks (bold, italic, code, links, etc.) through the Portable Text pipeline instead of stripping them to plain text. Links are sanitized viasanitizeHref(). Table styles now use CSS custom properties with fallbacks. -
Updated dependencies [
9ea4cf7,64f90d1,598026c]:- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- Adds InviteAcceptPage supporting passkey registration
Full changelog
Patch Changes
- #542
64f90d1Thanks @mohamedmostafa58! - Fixes invite flow: corrects invite URL to point to admin UI page, adds InviteAcceptPage for passkey registration.
- RTL (right-to-left) language support with LocaleDirectionProvider component
- JSON field editor added to admin UI content forms
Full changelog
Minor Changes
- #551
598026cThanks @ophirbucai! - Adds RTL (right-to-left) language support infrastructure. Enables proper text direction for RTL languages like Arabic, Hebrew, Farsi, and Urdu. Includes LocaleDirectionProvider component that syncs HTML dir/lang attributes with Kumo's DirectionProvider for automatic layout mirroring when locale changes.
Patch Changes
-
#489
9ea4cf7Thanks @all3f0r1! - Adds JSON field editor in admin UI content forms -
#542
64f90d1Thanks @mohamedmostafa58! - Fixes invite flow: corrects invite URL to point to admin UI page, adds InviteAcceptPage for passkey registration. -
Updated dependencies []:
- @emdash-cms/[email protected]
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
82c6345,64f90d1,598026c,197bc1b,ce873f8]:- [email protected]
- @emdash-cms/[email protected]
Fixes sandboxed plugin loading in Worker Loader by providing an `emdash` shim module.
Full changelog
- Enables the MCP server endpoint (`/_emdash/api/mcp`) by default; set `mcp: false` in config to disable
- Pre-bundles MCP SDK CJS dependencies for Cloudflare workerd to avoid "exports is not defined" crashes
- Adds `locale` to `ContentItem` type for i18n URL generation
- Adds `slug`, `status`, and `publishedAt` to `ContentItem` type; exports `ContentPublishStateChangeEvent` and fires `afterDelete` hooks
Full changelog
Minor Changes
-
#539
8ed7969Thanks @jdevalk! - Addslocaleto theContentItemtype returned by the plugin content access API. Follow-up to #536 — plugins that build i18n URLs from content records need the locale to pick the right URL prefix, otherwise multilingual content is emitted at default-locale URLs. -
#523
5d9120eThanks @jdevalk! - Addnlwebto the allowedrelvalues forpage:metadatalink contributions, letting plugins inject<link rel="nlweb" href="...">tags for agent/conversational endpoint discovery. -
#536
9318c56Thanks @ttmx! - Addsslug,status, andpublishedAtto theContentItemtype returned by the plugin content access API. ExportsContentPublishStateChangeEventtype. FiresafterDeletehooks on permanent content deletion. -
#519
5c0776dThanks @ascorbic! - Enables the MCP server endpoint by default. The endpoint at/_emdash/api/mcprequires bearer token auth, so it has no effect unless a client is configured. Setmcp: falseto disable.Fixes MCP server crash ("exports is not defined") on Cloudflare in dev mode by pre-bundling the MCP SDK's CJS dependencies for workerd.
Patch Changes
-
#515
5beddc3Thanks @ascorbic! - Reduces logged-out page load queries by caching byline existence, URL patterns, and redirect rules at worker level with proper invalidation. -
#512
f866c9cThanks @mahesh-projects! - Fixes save/publish race condition in visual editor toolbar. When a user blurred a field and immediately clicked Publish, the in-flight save PUT could arrive at the server after the publish POST, causing the stale revision to be promoted silently. IntroducespendingSavePromisesopublish()chains onto the pending save rather than firing immediately. -
#537
1acf174Thanks @Glacier-Luo! - Fixes plugin bundle resolving dist path before source, which caused build failures and potential workspace-wide source file destruction. -
#538
678cc8cThanks @Glacier-Luo! - Fixes revision pruning crash on PostgreSQL by replacing column alias in HAVING clause with the aggregate expression. -
#509
d56f6c1Thanks @mvanhorn! - Fixes TypeError when setting baseline security headers on Cloudflare responses with immutable headers. -
#495
2a7c68aThanks @ascorbic! - Fixes atomicity gaps: content update _rev check, menu reorder, byline delete, and seed content creation now run inside transactions. -
#497
6492ea2Thanks @ascorbic! - Fixes migration 011 rollback, plugin media upload returning wrong ID, MCP taxonomy tools bypassing validation, and FTS query escaping logic. -
#517
b382357Thanks @ascorbic! - Improves plugin safety: hooks log dependency cycles, timeouts clear timers, routes don't leak error internals, one-shot cron tasks retry with exponential backoff (max 5), marketplace downloads validate redirect targets. -
#532
1b743acThanks @ascorbic! - Fixes cold-start query explosion (159 -> ~25 queries) by short-circuiting migrations when all are applied, fixing FTS triggers to exclude soft-deleted content, and preventing false-positive FTS index rebuilds on every startup. -
Updated dependencies [
3a96aa7,c869df2,10ebfe1,275a21c,af0647c,b89e7f3,20b03b4,ba0a5af,e2f96aa,4645103]:- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- Complete i18n coverage of admin interface (1,216 message IDs)
- Added Basque, Portuguese (Brazil), and Chinese (Simplified) translations
- Client-side locale switching with dropdown selector
Full changelog
Minor Changes
Patch Changes
-
#490
3a96aa7Thanks @all3f0r1! - Fixes mobile sidebar nav sections not displaying their pages -
#87
c869df2Thanks @txhno! - Fixes SEO sidebar text fields firing a PUT on every keystroke by debouncing saves; guards against stale server responses overwriting newer local edits. -
#302
10ebfe1Thanks @ideepakchauhan7! - Fixes autosave form reset bug. Autosave no longer invalidates the query cache, preventing form fields from reverting to server state after autosave completes. -
#36
275a21cThanks @scottbuscemi! - Fixes image field removal not persisting after save by sending null instead of undefined, which JSON.stringify was silently dropping. -
#502
af0647cThanks @pagelab! - Adds Portuguese (Brazil) locale with full pt-BR translations following the WordPress pt-BR glossary standard. -
#521
b89e7f3Thanks @ascorbic! - Wraps all user-visible strings in the admin shell and core content screens with Lingui macros so they are translatable. Covers: Sidebar (nav labels, group headings), Header (View Site, Log out, Settings), ThemeToggle, Dashboard (headings, empty states, status indicators), ContentList (table headers, actions, dialogs, status badges), SaveButton, and ContentEditor (publish panel, schedule controls, byline editor, author selector, all dialogs). Runslocale:extractto add 116 new message IDs to all catalog files. -
#528
ba0a5afThanks @ascorbic! - Wraps all remaining admin UI components with Lingui macros, completing full i18n coverage of the admin interface. Catalog grows from 296 to 1,216 message IDs. Covers media library, menus, sections, redirects, taxonomies, content types, field editor, plugins, marketplace, SEO panels, setup wizard, auth flows, and all settings pages. -
#504
e2f96aaThanks @ascorbic! - Fixes client-side locale switching and replaces toggle buttons with a Select dropdown. -
#471
4645103Thanks @ayfl269! - Adds Chinese (Simplified) translation for the admin UI, including login page, settings page, and locale switching. -
Updated dependencies []:
- @emdash-cms/[email protected]
- Defensive identifier validation on all SQL interpolation points prevents injection via dynamic identifiers
- S3 storage config resolved from S3_* environment variables at runtime
Full changelog
Minor Changes
-
#457
f2b3973Thanks @UpperM! - Adds runtime resolution of S3 storage config fromS3_*environment
variables (S3_ENDPOINT,S3_BUCKET,S3_ACCESS_KEY_ID,
S3_SECRET_ACCESS_KEY,S3_REGION,S3_PUBLIC_URL). Any field omitted from
s3({...})is read from the matching env var on Node at runtime, so
container images can be built once and receive credentials at boot without a
rebuild. Explicit values ins3({...})still take precedence.s3()with no arguments is now valid for fully env-driven deployments.
accessKeyIdandsecretAccessKeyare now optional inS3StorageConfig
(both or neither). Workers users should continue passing explicit values to
s3({...}).
Patch Changes
-
#492
13f5ff5Thanks @UpperM! - Fixes manifest version being hardcoded to "0.1.0". The version and git commit SHA are now injected at build time via tsdown/Vitedefine, reading from package.json andgit rev-parse. -
#494
a283954Thanks @ascorbic! - Adds defensive identifier validation to all SQL interpolation points to prevent injection via dynamic identifiers. -
#351
c70f66fThanks @CacheMeOwside! - Fixes redirect loops causing the ERR_TOO_MANY_REDIRECTS error, by detecting circular chains when creating or editing redirects on the admin Redirects page. -
#499
0b4e61bThanks @ascorbic! - Fixes admin failing to load when installed from npm due to broken locale catalog resolution. -
Updated dependencies [
c70f66f,0b4e61b]:- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
f2b3973,13f5ff5,a283954,c70f66f,0b4e61b]:- [email protected]
- @emdash-cms/[email protected]
Minor fixes and improvements.
Full changelog
Patch Changes
-
#351
c70f66fThanks @CacheMeOwside! - Fixes redirect loops causing the ERR_TOO_MANY_REDIRECTS error, by detecting circular chains when creating or editing redirects on the admin Redirects page. -
#499
0b4e61bThanks @ascorbic! - Fixes admin failing to load when installed from npm due to broken locale catalog resolution. -
Updated dependencies []:
- @emdash-cms/[email protected]
Fixed GitHub OAuth login failure when user email is private.
Full changelog
Minor fixes and improvements.
Full changelog
Patch Changes
-
#463
701d0caThanks @sakibmd! - fix: replace code block with section in webhook notifier payload preview -
Updated dependencies [
156ba73,80a895b,da957ce,fcd8b7b,8ac15a4,ba2b020,0b108cf,1989e8b,e190324,724191c,ed28089,a293708,c75cc5b,6ebb797,d421ee2,391caf4,6474dae,30c9a96,122c236,5320321,8f44ec2,b712ae3,9cb5a28,7ee7d95,e1014ef,4d4ac53,476cb3a,87b0439,dd708b1,befaeec,c92e7e6,2ba1f1f,a13c4ec,a5e0603]:
- passkeyPublicOrigin config option removed (replace with siteUrl)
- Reject dangerous URL schemes in menu custom links
- Permission checks enforced on content status transitions, media provider endpoints, and translation group creation
- Plugin hooks: content:afterPublish and content:afterUnpublish
- Per-collection sitemaps with sitemap index and lastmod
- Repeater field type for structured repeating data
Full changelog
Minor Changes
-
#367
8f44ec2Thanks @ttmx! - Addscontent:afterPublishandcontent:afterUnpublishplugin hooks, fired after content is published or unpublished. Both are fire-and-forget notifications requiringread:contentcapability, supporting trusted and sandboxed plugins. -
#431
7ee7d95Thanks @jdevalk! - Per-collection sitemaps with sitemap index and lastmod/sitemap.xmlnow serves a<sitemapindex>with one child sitemap per SEO-enabled collection. Each collection's sitemap is at/sitemap-{collection}.xmlwith<lastmod>on both index entries and individual URLs. Uses the collection'surl_patternfor correct URL building. -
#414
4d4ac53Thanks @jdevalk! - Addsbreadcrumbs?: BreadcrumbItem[]toPublicPageContextso themes can publish a breadcrumb trail as part of the page context, and SEO plugins (or any otherpage:metadataconsumer) can read it without having to invent their own per-theme override mechanism.BreadcrumbItemis also exported from theemdashpackage root. The field is optional and non-breaking — existing themes and plugins work unchanged, and consumers can adopt it incrementally. Empty array (breadcrumbs: []) is an explicit opt-out signal (e.g. for homepages);undefinedmeans "no opinion, fall back to consumer's own derivation". -
#111
87b0439Thanks @mvanhorn! - Adds repeater field type for structured repeating data -
#382
befaeecThanks @UpperM! - AddssiteUrlconfig option to fix reverse-proxy origin mismatch. ReplacespasskeyPublicOriginwith a single setting that covers all origin-dependent features: passkeys, CSRF, OAuth, auth redirects, MCP discovery, snapshots, sitemap, robots.txt, and JSON-LD.Supports
EMDASH_SITE_URL/SITE_URLenvironment variables for container deployments where the domain is only known at runtime.Disables Astro's
security.checkOrigin(EmDash's own CSRF layer handles origin validation with dual-origin support and runtime siteUrl resolution). WhensiteUrlis set in config, also setssecurity.allowedDomainssoAstro.urlreflects the public origin in templates.Breaking:
passkeyPublicOriginis removed. Rename tositeUrlin yourastro.config.mjs.
Patch Changes
-
#182
156ba73Thanks @masonjames! - Fixes media routes so storage keys with slashes resolve correctly. -
#422
80a895bThanks @baezor! - Fixes SEO hydration exceeding D1 SQL variable limit on large collections by chunking thecontent_id IN (...)clause inSeoRepository.getMany. -
#94
da957ceThanks @eyupcanakman! - Reject dangerous URL schemes in menu custom links -
#223
fcd8b7bThanks @baezor! - Fixes byline hydration exceeding D1 SQL variable limit on large collections by chunking IN clauses. -
#479
8ac15a4Thanks @ascorbic! - Enforces permission checks on content status transitions, media provider endpoints, and translation group creation. -
#250
ba2b020Thanks @JULJERYT! - Optimize dashboard stats (3x fewer db queries) -
#340
0b108cfThanks @mvanhorn! - Passes emailPipeline to plugin route handler context so plugins with email:send capability can send email from route handlers. -
#148
1989e8bThanks @masonjames! - Adds public plugin settings helpers. -
#352
e190324Thanks @barckcode! - Allows external HTTPS images in the admin UI by addinghttps:to theimg-srcCSP directive. Fixes external content images (e.g. from migration or external hosting) being blocked in the content editor. -
#72
724191cThanks @travisbreaks! - Fix CLI login against remote Cloudflare-deployed instances by unwrapping API response envelope and adding admin scope -
#480
ed28089Thanks @ascorbic! - Fixes admin demotion guard, OAuth consent flow, device flow token exchange, preview token scoping, and revision cleanup on permanent delete. -
#247
a293708Thanks @NaeemHaque! - Fixes email settings page showing empty by registering the missing API route. Adds error state to the admin UI so fetch failures are visible instead of silently swallowed. -
#324
c75cc5bThanks @barckcode! - Fixes admin editor crash when image blocks lack theassetwrapper. Image blocks withurlat the top level (e.g. from CMS migrations) now render correctly instead of throwingTypeError: Cannot read properties of undefined (reading 'url'). -
#353
6ebb797Thanks @ilicfilip! - fix(core): pass field.options through to admin manifest for plugin field widgets -
#209
d421ee2Thanks @JonahFoster! - Fixes base OG, Twitter, and article JSON-LD titles so they can use a page-specific title without including the site name suffix from the document title. -
#394
391caf4Thanks @datienzalopez! - Fixesplugin:activateandplugin:deactivatehooks not being called when enabling or disabling a plugin via the admin UI orsetPluginStatus. Previously,setPluginStatusrebuilt the hook pipeline but never invoked the lifecycle hooks. Nowplugin:activatefires after the pipeline is rebuilt with the plugin included, andplugin:deactivatefires on the current pipeline before the plugin is removed. -
#357
6474daeThanks @Vallhalen! - Fix: default adminPages and dashboardWidgets to empty arrays in manifest to prevent admin UI crash when plugins omit these properties. -
#453
30c9a96Thanks @all3f0r1! - Fixesctx.content.create()andctx.content.update()so plugins can write
to the core SEO panel. When the inputdatacontains a reservedseokey,
it is now extracted and routed to_emdash_seovia the SEO repository,
matching the REST API shape.ctx.content.get()andctx.content.list()
also hydrate theseofield on returned items for SEO-enabled collections. -
#326
122c236Thanks @barckcode! - Fixes WXR import not preserving original post dates or publish status. Useswp:post_date_gmt(UTC) with fallback chain topubDate(RFC 2822) thenwp:post_date(site-local). Handles the WordPress0000-00-00 00:00:00sentinel for unpublished drafts. Setspublished_atfor published posts. Applies to both WXR file upload and plugin-based import paths. -
#371
5320321Thanks @pejmanjohn! - Fix MCP OAuth discovery for unauthenticated POST requests. -
#338
b712ae3Thanks @mvanhorn! - Fixes standalone wildcard "" in plugin allowedHosts so plugins declaring allowedHosts: [""] can make outbound HTTP requests to any host. -
#434
9cb5a28Thanks @hayatosc! - Avoid accessing sessions on prerendered public routes. -
#119
e1014efThanks @blmyr! - Fix pluginpage:metadataandpage:fragmentshooks not firing for anonymous public page visitors. The middleware's early-return fast-path for unauthenticated requests now initializes the runtime (skipping only the manifest query), so plugin contributions render via<EmDashHead>,<EmDashBodyStart>, and<EmDashBodyEnd>for all visitors. Also addscollectPageMetadataandcollectPageFragmentsto theEmDashHandlersinterface. -
#424
476cb3aThanks @csfalcao! - Fixes public access to the search API (#104). The auth middleware blocked/_emdash/api/searchbefore the handler ran, so #107's handler-level change never took effect for anonymous callers. Adds the endpoint toPUBLIC_API_EXACTso the shippedLiveSearchcomponent works on public sites without credentials. Admin endpoints (/search/enable,/search/rebuild,/search/stats,/search/suggest) remain authenticated. -
#333
dd708b1Thanks @mvanhorn! - Adds composite index on (deleted_at, published_at DESC, id DESC) to eliminate full table scans for frontend listing queries that order by published_at. -
#448
c92e7e6Thanks @grexe! - fixes logo and favicon site settings not being applied to templates -
#319
2ba1f1fThanks @ideepakchauhan7! - Fixes i18n config returning null in Vite dev SSR by reading from virtual module instead of dynamic import. -
#251
a13c4ecThanks @yohaann196! - fix: expose client_id in device flow discovery response -
#93
a5e0603Thanks @eyupcanakman! - Fix taxonomy links missing from admin sidebar -
Updated dependencies [
0966223,53dec88,3b6b75b,a293708,1a93d51,c9bf640,87b0439,5eeab91,e3f7db8,a5e0603]:- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
156ba73,80a895b,da957ce,fcd8b7b,8ac15a4,ba2b020,0b108cf,1989e8b,e190324,724191c,ed28089,a293708,c75cc5b,6ebb797,d421ee2,391caf4,6474dae,30c9a96,122c236,5320321,8f44ec2,b712ae3,9cb5a28,7ee7d95,e1014ef,4d4ac53,476cb3a,e1349e3,87b0439,dd708b1,befaeec,c92e7e6,2ba1f1f,a13c4ec,a5e0603]:- [email protected]
- @emdash-cms/[email protected]
- Repeater field type for structured repeating data
Full changelog
Minor Changes
Patch Changes
-
#467
0966223Thanks @sakibmd! - fix: move useMemo above early returns in ContentListPage -
#349
53dec88Thanks @tsikatawill! - Fixes menu editor rejecting relative URLs like /about by changing input type from url to text with pattern validation. -
#99
3b6b75bThanks @all3f0r1! - Fix content list not fetching beyond the first API page when navigating to the last client-side page -
#247
a293708Thanks @NaeemHaque! - Fixes email settings page showing empty by registering the missing API route. Adds error state to the admin UI so fetch failures are visible instead of silently swallowed. -
#316
c9bf640Thanks @mvanhorn! - Allow relative URLs in menu custom links by changing input type from "url" to "text" -
#377
5eeab91Thanks @Pouf5! - Fixes new content always being created with localeenregardless of which locale is selected in the collection locale switcher. The "Add New" link now forwards the active locale to the new-content route, and the new-content page passes it through to the create API. -
#185
e3f7db8Thanks @ophirbucai! - Fixes field scroll-into-view not triggering when navigating to a field via URL parameter. -
#93
a5e0603Thanks @eyupcanakman! - Fix taxonomy links missing from admin sidebar -
Updated dependencies [
e1349e3]:- @emdash-cms/[email protected]
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
156ba73,80a895b,da957ce,fcd8b7b,8ac15a4,ba2b020,0b108cf,1989e8b,e190324,724191c,ed28089,a293708,c75cc5b,6ebb797,d421ee2,391caf4,6474dae,30c9a96,122c236,5320321,8f44ec2,b712ae3,9cb5a28,7ee7d95,e1014ef,4d4ac53,476cb3a,87b0439,dd708b1,befaeec,c92e7e6,2ba1f1f,a13c4ec,a5e0603]:
Minor fixes and improvements.
Full changelog
Patch Changes
- #246
e1349e3Thanks @estepanov! - Adds overflow-hidden and text-ellipsis to field value elements, with a title attribute for full-text tooltip on hover
Minor fixes and improvements.
The create-emdash CLI no longer suggests manually running the bootstrap step.
Full changelog
New features and improvements in emdash.
Full changelog
Minor fixes and improvements.
Minor fixes and improvements.
Full changelog
New features and improvements in emdash.
Full changelog
- First beta release of AT Protocol plugin
Full changelog
- First beta release of color plugin
Full changelog
New features and improvements in emdash.
Full changelog
Minor fixes and improvements.
Minor fixes and improvements.
- First beta release of audit log plugin
Full changelog
New features and improvements in emdash.
Full changelog
New features and improvements in emdash.
Full changelog
Minor Changes
Patch Changes
- Updated dependencies [
755b501]:- [email protected]
- @emdash-cms/[email protected]
- First beta release
Full changelog
Minor Changes
Patch Changes
- Updated dependencies [
755b501]:- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
- @emdash-cms/[email protected]
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
3c319ed]:
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
3c319ed]:
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
3c319ed]:
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
3c319ed]:
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
3c319ed]:
Fixes crash on fresh deployments when public pages accessed before setup runs.
Full changelog
Patch Changes
-
#8
3c319edThanks @ascorbic! - Fix crash on fresh deployments when the first request hits a public page before setup has run. The middleware now detects an empty database and redirects to the setup wizard instead of letting template helpers query missing tables. -
Updated dependencies [
3c319ed]:- @emdash-cms/[email protected]
Minor fixes and improvements.
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
3c319ed]:
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
3c319ed]:
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
3c319ed]:
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
b09bfd5]:
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
b09bfd5]:
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
b09bfd5]:
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
b09bfd5]:
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
b09bfd5]:
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
b09bfd5]:
- EmDash branded banner displayed on CLI startup
- Auto‑detects and allows selection of package manager (npm, yarn, pnpm)
- Interactive prompt to install dependencies with progress spinner
Full changelog
Minor fixes and improvements.
Full changelog
Patch Changes
- Updated dependencies [
b09bfd5]: