ci: release#1283
Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | 905bdcd | Jun 03 2026, 10:20 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | 905bdcd | Jun 03 2026, 10:20 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 905bdcd | Jun 03 2026, 10:20 PM |
Scope checkThis PR touches 29 files. PRs with a broad scope are harder to review. Please confirm the scope hasn't drifted beyond the intended change. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
5cdf1c5 to
8945b2b
Compare
5828830 to
7256dec
Compare
c3ad116 to
660236f
Compare
660236f to
905bdcd
Compare
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@emdash-cms/admin@0.17.0
Minor Changes
#1258
28432b9Thanks @MohamedH1998! - Adds custom fields to bylines. Sites can define site-specific byline metadata (Twitter handle, pronouns, company, localised job title, etc.) via the new/byline-schemaadmin screen, accessed from the Byline schema link button at the top of the Bylines admin page (admin-only).Per-field
translatableflag picks whether values are stored per-locale (one value per locale row in atranslation_group) or shared across every locale variant of the same byline identity. Schema management is gated byschema:manage; value editing bybylines:manage.Custom-field values can be set at both create and update time.
POSTandPUTon/_emdash/api/admin/bylinesaccept the samecustomFieldsmap; the row write and the custom-field writes share a single transaction on Node/PG so a partial failure rolls both back. On D1 (no transactions), a retry POST is treated as completing an abandoned create iff three checks all pass: (a) every fixed column on the existing row matches the new payload (displayName,bio,avatarMediaId,websiteUrl,userId,isGuest, effective locale — null-vs-undefined normalised); (b) the existing row'stranslationGroupmatches what a fresh create with the same input would produce (sourceGroupwhentranslationOfis present,existing.idwhen it isn't); (c) every custom-field value already stored on the row appears in the input payload with an equal value (subset-match, so partial mid-loop crashes can be completed). The recovery branch is conservative on every axis: any fixed-column mismatch, any translation-group mismatch, any overlapping custom-field value mismatch, an input that omits a key the existing row stores, or an input with no custom fields at all → standardCONFLICT. Validation runs before any DB write so a bad value (unknown slug, type mismatch, select-choice miss, non-URL or non-http(s) URL for aurlfield) returns 400VALIDATION_ERRORwithout leaving partial state behind. In the admin, registered fields render inline with Name, Bio, etc. — no separate section header — and are available in the New byline dialog as well as edit.BylineSummarygains an optionalcustomFields: Record<string, CustomFieldValue>property. Existing object-literal consumers stay source-compatible because the property is optional and runtime always returns{}when no fields are registered.Hydration is symmetric with writes: rows are only applied to a byline when they live in the table matching the field's current
translatableflag, so stale rows from atranslatableflip can't leak into hydrated output. Schema mutations on/byline-schemainvalidate the samebyline-fieldsquery the byline form reads, so newly-registered fields appear in the editor without a page reload.urlfield values are parsed withnew URL(...)AND restricted tohttp:/https:schemes at write time so they can't shipjavascript:/data:/mailto:payloads to link rendering. TheBylineFieldEditor"Save" button stays disabled until aselectfield has at least one option; and select-option lists are accumulated on a null-prototype object so option values that collide withObject.prototypekeys render correctly.The field-definitions cache uses parity on
options.byline_fields_versionas a dirty bit: schema mutations flip the counter to odd before the write lands and to a new even value after, with the cache treating any odd version as "bypass the global holder, read fresh from the DB".markVersionDirtyis parity-aware (ensures odd, no-op if already odd) so a crashed prior attempt's leftover dirty state can't get inverted.markVersionCleanis always-advance (+2when starting even,+1when starting odd) so two concurrent mutators can't collapse on the same even key and pin the cache on a partial-set snapshot — every committed mutation produces an observable counter change for cache readers. Idempotent-retry exits (FIELD_EXISTSon create,FIELD_NOT_FOUNDon update/delete, no-op input on update) callmarkVersionCleantoo, which doubles as both the dirty-crash recovery and the false-clean recovery. All version writes useINSERT … ON CONFLICT DO UPDATEso a missing options row can't silently turn invalidation into a no-op.Implements #1174. Builds on the bylines-i18n foundation from #1146.
#1215
590b2f9Thanks @scottbuscemi! - First-class HTML block in the admin editor. The existinghtmlBlockPortable Text type (produced by the WordPress and Contentful importers) is now a fully editable block in the rich-text editor. Authors can insert an HTML block via the/htmlslash command and edit raw HTML in a textarea. ImportedhtmlBlockcontent that previously fell through to an opaquepluginBlockplaceholder is now rendered in the same editable UI. The inline (visual-editing) editor preserves HTML blocks as read-only placeholders to prevent data loss.Patch Changes
#1297
cccf4f2Thanks @emdashbot! - Fix content entity editor block menu actions (Duplicate / Delete) not working when the drag handle sets a NodeSelection (#1261).#1225
886f2d1Thanks @scottbuscemi! - Add search to the byline picker on content entities and remove the effective 100-byline cap. The picker now performs a debounced server-side search via the bylines API instead of rendering a fixed dropdown of the first 100 results, so bylines beyond the first page can be found and credited. Credited bylines from the saved entry are also resolved from the entry itself, so a credit that falls outside the initial list still renders its name instead of disappearing.#1222
a5dafb3Thanks @scottbuscemi! - Fixes the byline search box reloading the whole page on every keystroke. The search term is now debounced (300ms) before it feeds the bylines query, and the full-page loader only takes over when there is no data yet (isLoading && !data) instead of on every new query key. Typing now stays responsive and keeps the input focused, matching the behaviour of the users page. The load-more snapshot and its filter-match check both use the debounced search value so appended pages are no longer discarded.#1226
9422d6aThanks @scottbuscemi! - Make content list search work on large collections (#1219). The admin content list previously filtered only the rows already loaded on the current page, so an entry far back in a big collection could not be found until you navigated near it. The list endpoint now accepts aqparameter and performs a case-insensitive substring search across the collection's title/name/slug columns server-side (LIKE wildcards in the query are escaped), and the admin search box drives that query (debounced) instead of filtering in memory. Also adds locale-aware composite indexes (idx_{table}_loc_upd/idx_{table}_loc_crt) so locale-filtered content lists stay index-served on large, i18n-enabled tables.#1224
67f5992Thanks @scottbuscemi! - Fix taxonomy terms not being locale-aware in the content editor (#1218). Term assignments are stored against the per-locale content row while the term'stranslation_groupspans every locale, so resolving terms for an entry must scope to the entry's locale. The content terms endpoint (/content/:collection/:id/terms/:taxonomy) now derives the entry's locale server-side and passes it togetTermsForEntry, and the adminTaxonomySidebarthreads the entry locale through its fetch/save calls (and into its React Query keys, so switching translations refetches). Previously a localized post showed and applied every locale variant of a tag instead of just the variant for its own locale.#1227
a40e455Thanks @scottbuscemi! - Add search and filtering to the media library (#1221). The media list endpoint now accepts aqparameter for a case-insensitive filename substring search (which also matches extensions, with LIKE wildcards escaped), alongside the existingmimeTypefilter. The Media Library page gains a filename search box and a type filter (images / video / audio / documents), and the media picker in the content editor now searches the local library by filename too. Previously neither surface could search or filter local media, which made large libraries hard to navigate.#1223
34afc14Thanks @scottbuscemi! - The rich-text editor formatting toolbar now stays pinned to the top of the editing area while scrolling through long posts, instead of scrolling out of view. The toolbar usesposition: stickyand the editor wrapper switched fromoverflow-hiddentooverflow-clipso corners stay clipped without creating a nested scroll container that would break sticky positioning. Distraction-free / minimal editors (e.g. Widgets) are unaffected since they don't render the toolbar.#1309
019d9e4Thanks @ahliweb! - Fix the remaining Indonesian admin translations.#1310
ba0f3d4Thanks @emdashbot! - Fix "Add Content" in the menu editor: the admin was sending the raw collection slug (e.g.pages) as the menu itemtype, which the API'smenuItemTypeEnumrejects with a 400. Map the picker's collection to the correct enum value (pages→page,posts→post, everything else →collection) so picking a page or post actually adds it to the menu. (#1173)#1290
aacdf20Thanks @SL33PiNg! - i18n(th): translate remaining untranslated Thai (ไทย) admin UI strings, including the newly-added plugin registry and Portable Text HTML-block strings#1245
7d55db6Thanks @SL33PiNg! - Adds Thai (ไทย) locale to the admin UI.Updated dependencies [
69bdc97]:emdash@0.17.0
Minor Changes
#1258
28432b9Thanks @MohamedH1998! - Adds custom fields to bylines. Sites can define site-specific byline metadata (Twitter handle, pronouns, company, localised job title, etc.) via the new/byline-schemaadmin screen, accessed from the Byline schema link button at the top of the Bylines admin page (admin-only).Per-field
translatableflag picks whether values are stored per-locale (one value per locale row in atranslation_group) or shared across every locale variant of the same byline identity. Schema management is gated byschema:manage; value editing bybylines:manage.Custom-field values can be set at both create and update time.
POSTandPUTon/_emdash/api/admin/bylinesaccept the samecustomFieldsmap; the row write and the custom-field writes share a single transaction on Node/PG so a partial failure rolls both back. On D1 (no transactions), a retry POST is treated as completing an abandoned create iff three checks all pass: (a) every fixed column on the existing row matches the new payload (displayName,bio,avatarMediaId,websiteUrl,userId,isGuest, effective locale — null-vs-undefined normalised); (b) the existing row'stranslationGroupmatches what a fresh create with the same input would produce (sourceGroupwhentranslationOfis present,existing.idwhen it isn't); (c) every custom-field value already stored on the row appears in the input payload with an equal value (subset-match, so partial mid-loop crashes can be completed). The recovery branch is conservative on every axis: any fixed-column mismatch, any translation-group mismatch, any overlapping custom-field value mismatch, an input that omits a key the existing row stores, or an input with no custom fields at all → standardCONFLICT. Validation runs before any DB write so a bad value (unknown slug, type mismatch, select-choice miss, non-URL or non-http(s) URL for aurlfield) returns 400VALIDATION_ERRORwithout leaving partial state behind. In the admin, registered fields render inline with Name, Bio, etc. — no separate section header — and are available in the New byline dialog as well as edit.BylineSummarygains an optionalcustomFields: Record<string, CustomFieldValue>property. Existing object-literal consumers stay source-compatible because the property is optional and runtime always returns{}when no fields are registered.Hydration is symmetric with writes: rows are only applied to a byline when they live in the table matching the field's current
translatableflag, so stale rows from atranslatableflip can't leak into hydrated output. Schema mutations on/byline-schemainvalidate the samebyline-fieldsquery the byline form reads, so newly-registered fields appear in the editor without a page reload.urlfield values are parsed withnew URL(...)AND restricted tohttp:/https:schemes at write time so they can't shipjavascript:/data:/mailto:payloads to link rendering. TheBylineFieldEditor"Save" button stays disabled until aselectfield has at least one option; and select-option lists are accumulated on a null-prototype object so option values that collide withObject.prototypekeys render correctly.The field-definitions cache uses parity on
options.byline_fields_versionas a dirty bit: schema mutations flip the counter to odd before the write lands and to a new even value after, with the cache treating any odd version as "bypass the global holder, read fresh from the DB".markVersionDirtyis parity-aware (ensures odd, no-op if already odd) so a crashed prior attempt's leftover dirty state can't get inverted.markVersionCleanis always-advance (+2when starting even,+1when starting odd) so two concurrent mutators can't collapse on the same even key and pin the cache on a partial-set snapshot — every committed mutation produces an observable counter change for cache readers. Idempotent-retry exits (FIELD_EXISTSon create,FIELD_NOT_FOUNDon update/delete, no-op input on update) callmarkVersionCleantoo, which doubles as both the dirty-crash recovery and the false-clean recovery. All version writes useINSERT … ON CONFLICT DO UPDATEso a missing options row can't silently turn invalidation into a no-op.Implements #1174. Builds on the bylines-i18n foundation from #1146.
#1215
590b2f9Thanks @scottbuscemi! - First-class HTML block in the admin editor. The existinghtmlBlockPortable Text type (produced by the WordPress and Contentful importers) is now a fully editable block in the rich-text editor. Authors can insert an HTML block via the/htmlslash command and edit raw HTML in a textarea. ImportedhtmlBlockcontent that previously fell through to an opaquepluginBlockplaceholder is now rendered in the same editable UI. The inline (visual-editing) editor preserves HTML blocks as read-only placeholders to prevent data loss.Patch Changes
#1298
cd2dcc6Thanks @ascorbic! - Byline hydration now resolves the author avatar's storage key in the same query.getEmDashCollection/getEmDashEntrypopulateentry.data.bylines[].byline.avatarStorageKey(andavatarAlt) via aLEFT JOINon the media table, so list pages can build a direct avatar URL without a per-bylineMediaRepository.findById. Previously the byline summary exposed onlyavatarMediaId(a bare ULID with no file extension), forcing sites that want direct storage URLs into an N+1 media lookup. A page rendering 20 posts by distinct authors paid ~20 extra queries. The new fields are additive and null on the plain byline finders (findById,findBySlug), which do not join media; rely on the content-credit hydration path for them.#1197
62c170fThanks @scottbuscemi! - Persist welcome-dismissed flag in database instead of session. Previously the welcome modal would be shown every time a user logged-in.#1295
ee67273Thanks @emdashbot! - fix(core/redirects): match exact redirects regardless of trailing slash (#1271)Exact redirect rules now match requests with or without a trailing slash. A redirect stored with source
/old/will also match a request for/old, and a redirect stored with source/oldwill also match/old/. The stored source is preserved unchanged; the fallback happens at lookup time.#1226
9422d6aThanks @scottbuscemi! - Make content list search work on large collections (#1219). The admin content list previously filtered only the rows already loaded on the current page, so an entry far back in a big collection could not be found until you navigated near it. The list endpoint now accepts aqparameter and performs a case-insensitive substring search across the collection's title/name/slug columns server-side (LIKE wildcards in the query are escaped), and the admin search box drives that query (debounced) instead of filtering in memory. Also adds locale-aware composite indexes (idx_{table}_loc_upd/idx_{table}_loc_crt) so locale-filtered content lists stay index-served on large, i18n-enabled tables.#1302
1f8190dThanks @WellDunDun! - Fixes locale-aware content updates so REST, CLI, client, and MCP callers can safely update content by slug when multiple locales share the same slug.#1224
67f5992Thanks @scottbuscemi! - Fix taxonomy terms not being locale-aware in the content editor (#1218). Term assignments are stored against the per-locale content row while the term'stranslation_groupspans every locale, so resolving terms for an entry must scope to the entry's locale. The content terms endpoint (/content/:collection/:id/terms/:taxonomy) now derives the entry's locale server-side and passes it togetTermsForEntry, and the adminTaxonomySidebarthreads the entry locale through its fetch/save calls (and into its React Query keys, so switching translations refetches). Previously a localized post showed and applied every locale variant of a tag instead of just the variant for its own locale.#1227
a40e455Thanks @scottbuscemi! - Add search and filtering to the media library (#1221). The media list endpoint now accepts aqparameter for a case-insensitive filename substring search (which also matches extensions, with LIKE wildcards escaped), alongside the existingmimeTypefilter. The Media Library page gains a filename search box and a type filter (images / video / audio / documents), and the media picker in the content editor now searches the local library by filename too. Previously neither surface could search or filter local media, which made large libraries hard to navigate.#1319
69bdc97Thanks @ascorbic! - Fixrequire is not definedcrash on every EmDash API route underastro devon Cloudflare Workers (#1292).@emdash-cms/registry-clientlistedsemver(CommonJS) independencies, which the build externalizes -- so consumers loaded a nested CJS copy. Vite's SSR module runner (workerd) evaluates modules with norequirebinding, so semver's internalrequire()threw and took down any route whose import graph reached registry-client (schema, plugins, env compatibility checks). semver is now bundled into the ESM output, so nothing CommonJS reaches the worker.#1285
5e7f835Thanks @ascorbic! - Fix SEO fields (noindex toggle, canonical URL) not affecting rendered pages. The content loader now surfaces per-entry SEO metadata onentry.data.seo, sogetSeoMeta()reflects values set in the admin SEO panel. SEO is folded into the existing entry query via a LEFT JOIN, adding no extra database round-trips.#1298
cd2dcc6Thanks @ascorbic! - Seed files can now attach an avatar to a byline.bylines[].avatartakes astorageKey(plus optionalalt,filename,mimeType,width,height) for a file that already exists in the configured storage; applying the seed creates amediarow and links it to the byline viaavatarMediaId. Unlike a content$mediareference, nothing is downloaded or uploaded, which suits seeding bylines alongside a media migration.Updated dependencies [
cccf4f2,28432b9,886f2d1,a5dafb3,9422d6a,67f5992,a40e455,69bdc97,34afc14,590b2f9,019d9e4,ba0f3d4,aacdf20,7d55db6]:@emdash-cms/auth-atproto@0.2.11
Patch Changes
@emdash-cms/cloudflare@0.17.0
Patch Changes
cd2dcc6,62c170f,ee67273,28432b9,9422d6a,1f8190d,67f5992,a40e455,69bdc97,5e7f835,590b2f9,cd2dcc6]:@emdash-cms/plugin-cli@0.5.1
Patch Changes
69bdc97]:@emdash-cms/plugin-embeds@0.1.19
Patch Changes
@emdash-cms/registry-client@0.3.1
Patch Changes
#1319
69bdc97Thanks @ascorbic! - Fixrequire is not definedcrash on every EmDash API route underastro devon Cloudflare Workers (#1292).@emdash-cms/registry-clientlistedsemver(CommonJS) independencies, which the build externalizes -- so consumers loaded a nested CJS copy. Vite's SSR module runner (workerd) evaluates modules with norequirebinding, so semver's internalrequire()threw and took down any route whose import graph reached registry-client (schema, plugins, env compatibility checks). semver is now bundled into the ESM output, so nothing CommonJS reaches the worker.@emdash-cms/sandbox-workerd@0.1.3
Patch Changes
cd2dcc6,62c170f,ee67273,28432b9,9422d6a,1f8190d,67f5992,a40e455,69bdc97,5e7f835,590b2f9,cd2dcc6]:@emdash-cms/auth@0.17.0
@emdash-cms/blocks@0.17.0
create-emdash@0.17.0
@emdash-cms/gutenberg-to-portable-text@0.17.0
@emdash-cms/x402@0.17.0
@emdash-cms/fixture-perf-site@0.0.14
Patch Changes
cd2dcc6,62c170f,ee67273,28432b9,9422d6a,1f8190d,67f5992,a40e455,69bdc97,5e7f835,590b2f9,cd2dcc6]:@emdash-cms/perf-demo-site@0.0.14
Patch Changes
cd2dcc6,62c170f,ee67273,28432b9,9422d6a,1f8190d,67f5992,a40e455,69bdc97,5e7f835,590b2f9,cd2dcc6]:@emdash-cms/cache-demo-site@0.0.14
Patch Changes
cd2dcc6,62c170f,ee67273,28432b9,9422d6a,1f8190d,67f5992,a40e455,69bdc97,5e7f835,590b2f9,cd2dcc6]: