Minor Changes
-
#3680
75de9a4Thanks @khoinguyenpham04! - Adds a publishing calendar to the admin. It shows published entries on their publication date, and scheduled entries and scheduled updates on their scheduled date, across every visible collection and locale. Contributors and higher roles open it from Calendar in the sidebar, the command palette, or the dashboard's Scheduled count.Month shows a grid of days and Agenda lists entries by day; phones and other narrow screens open the agenda and show the month as a date picker. Both views place entries in the site's time zone, show browser-zone times when the viewer's zone differs, mark schedules that missed their time as Overdue, filter by collection, locale, and state, and keep the view, month, filters, and open entry in the URL. When a month's grid holds more than 1,000 entries, the calendar shows the first 1,000 and marks the days it didn't load.
Selecting an entry opens a side panel with its details, a link to the editor, and a preview or live link. Users who can publish the entry can also reschedule it, remove its schedule, or publish an overdue entry immediately. Ctrl-click or Cmd-click opens the entry in the editor instead.
-
#3566
3c70ca5Thanks @stephanedemotte! - AddslocaleandtranslationOfto thecontent:beforeSaveandcontent:afterSavehook events, for trusted and sandboxed plugins, so a hook can tell a new entry from a new translation of an existing one.localeis the locale the entry is saved in: the resolved requested locale (or the default locale) on a create, and the stored entry's locale on an update.translationOfis the ID of the source entry when a create comes from the translation flow, and is absent otherwise. Both fields are optional; existing hooks are unaffected. -
#3687
76b06d4Thanks @khoinguyenpham04! - Adds CSS and JavaScript to HTML blocks, and changes how new HTML blocks render on the site.Previously, every HTML block rendered inline, and the site sanitized its HTML, removing scripts and styles. HTML blocks created in the admin editor, or with
/htmlin visual editing, now render in a sandboxed iframe that runs their HTML, CSS and JavaScript. Anyone who can edit content (Contributor and up, and sandboxed plugins withcontent:write) can add JavaScript that runs for visitors once the entry is published. The iframe can't read the site's cookies, storage or pages, and the site's styles don't apply inside it. Relative links in it resolve from the site's root. After a visitor clicks inside the iframe, it can open other pages in the visitor's tab or a new one. There is no site-wide setting for this; to render a block the previous way, choose Inline in its menu.Existing HTML blocks, and blocks created through imports, REST or MCP, keep rendering inline unless they set
isolated: true.In the editor, an HTML block has HTML, CSS and JS tabs with a code editor, and a Preview tab that shows the block as the site renders it. A saved block that runs JavaScript waits for Run preview. The admin editor no longer nests HTML blocks in quotes, lists or table cells, where saving dropped them. HTML blocks pasted from another website or browser tab render inline.
New fields
htmlBlockgains optionalcssandjsstrings and anisolatedflag. They're written only when set, so existing content is unchanged when it's opened and saved.What should I do?
- If your site replaces the
htmlBlockrenderer, pass blocks withisolated: truetoHtmlBlockfromemdash/ui, or render them in an iframe whosesandboxomitsallow-same-origin. Otherwise isolated blocks render without their CSS and JavaScript. - If you render Portable Text outside EmDash's components, handle
isolatedblocks the same way. - With Astro's content security policy turned on, the browser blocks the styles and scripts inside isolated blocks, so they render without their CSS, JavaScript or automatic height.
- If your site replaces the
-
#3688
a880323Thanks @khoinguyenpham04! - Adds an iframe block for embedding pages from other sites. In the editor, type/iframeand paste an embed code or a link into the Code tab; YouTube and Vimeo links become their players, and the Preview tab shows the embedded page. When an entry opens, a saved block waits for Load preview. Iframe blocks pasted from another website or browser tab arrive empty. On the site,Iframefromemdash/uirenders theiframeblock as a responsive, lazy-loading iframe.The iframe is sandboxed and sends a
strict-origin-when-cross-originreferrer. Only https sources render, pages on the site's own host lose same-origin access, and only the permissions video and map players need (such as autoplay, fullscreen and picture-in-picture) reach the embedded page.The admin's content security policy now allows https frames (
frame-src 'self' https:), so previews can load embedded pages, including frames inside HTML block previews.What should I do?
- If a plugin already defines an
iframeblock, the editor keeps using the plugin's block and doesn't offer the built-in one. On the site, the plugin's renderer still wins; a plugin without one getsIframefor blocks that have only the built-in fields. In TypeScript, narrowingPortableTextBlockon_type === "iframe"now givesPortableTextIframeBlock | PortableTextUnknownBlock, so reading the plugin's own fields needs a check such as"theme" in block. - With Astro's content security policy turned on, allow the embedded hosts in
frame-src. Iframe blocks also lose their custom size under that policy.
- If a plugin already defines an
-
#3254
5de3bdcThanks @danielmlr! - Adds amicrosoft()login provider, so editors can sign in to the admin with a Microsoft Entra ID work or school account next to passkeys and the other providers.import { microsoft } from "emdash/auth/providers/microsoft"; emdash({ authProviders: [microsoft()] });
The provider reads
EMDASH_OAUTH_MICROSOFT_CLIENT_ID,EMDASH_OAUTH_MICROSOFT_CLIENT_SECRET, andEMDASH_OAUTH_MICROSOFT_TENANT_ID(or the unprefixed names) and stays unconfigured until all three are set. The tenant is a directory (tenant) ID, orcommon,organizations, orconsumers. With a directory ID, an account that signs in through that directory counts as verified when its address is in the domain of its sign-in name, or when the optionalxms_edovclaim confirms the address's domain, so it can link to an existing user, accept an invite, sign up through an allowed domain, and create the first admin account. Accounts that sign in through another directory or identity provider, such as guests, and sign-ins throughcommon,organizations, orconsumersdo not count as verified.microsoft({ emailVerified })overrides this either way. -
#3632
728790bThanks @swissky! - Adds aWebMcpSearchcomponent (emdash/ui/webmcp-search) that lets AI agents in a visitor's browser search your published content. In browsers that support WebMCP, it registers a read-onlysearch_sitetool backed by the public search API and returns titles, absolute URLs, and plain-text excerpts. It accepts the samecollections,locale,limit, androuteMapprops asLiveSearchand does nothing in other browsers.
Patch Changes
-
#3526
2d84db5Thanks @danielmlr! - Fixes byline translations dropping the source byline's linked user, which left the user's entries in the translation's locale without an author credit. Translations created with the admin's Translate action now keep the source's user, so those entries show the translated byline.When
translationOfis set and the call omitsuserId, thebyline_createMCP tool now links the source's user instead of creating an unlinked translation. PassuserId: nullto keep the previous behavior.A user can have only one byline per locale. Creating a byline with a
userIdthat already has a different byline in the target locale fails withCONFLICTnaming that byline, instead of a server error. Translating a byline whose user already has a different byline in the target locale also fails withCONFLICT, where it previously created an unlinked translation.What should I do?
Translations created before this release stay unlinked, and the admin's byline list marks them as unlinked. To restore author credits in a locale, open the translation in the admin and link the user.
If Translate reports that the user is already linked to another byline in that locale, unlink that byline in the admin first, or create the translation with
byline_createanduserId: null. -
#3711
2436119Thanks @swissky! - Fixes the setup wizard failing withSITE_URL_REQUIREDon Cloudflare Workers since 0.40.1 whensiteUrl,EMDASH_SITE_URL, orSITE_URLis not set, which blocked new sites created with the Deploy to Cloudflare button. On Cloudflare's network, setup now recordshttps://and the hostname it runs on as the origin for authentication emails, and passkeys created during setup only work on that hostname.- To use a custom domain, run setup on that domain or set
EMDASH_SITE_URLfirst. - Node.js deployments still need a configured origin before production setup.
- A Workers build served outside Cloudflare (
wrangler dev,astro preview, or self-hosted workerd) on a reachable address should setsiteUrlorEMDASH_SITE_URLbefore setup.
- To use a custom domain, run setup on that domain or set
-
#3580
0cfa989Thanks @emdashbot! - Addsimage/jxl(JPEG XL) to the default media upload allowlist, so.jxlfiles can be uploaded without a field-specific MIME list. Upload routes now fall back to the filename extension when the browser reports an empty or generic MIME type, which is the common case for JPEG XL outside Safari. -
#3652
2ef2d01Thanks @danielmlr! - Speeds up the image endpoint EmDash installs (/_imageby default): its requests no longer wait for the database setup check and runtime startup that delayed the first images on a fresh server instance, such as a new Cloudflare Worker isolate. Signed-in requests still getlocals.user, but on D1 withsessionenabled their responses no longer set the D1 bookmark cookie, so an edge-cache route rule for/_imagenow also caches admin media thumbnails.On these requests
locals.emdashholds onlystorage, plusdbfor a signed-in user; the page and admin helpers it carried before are not set. Playground requests are unchanged. Whilecheckormanualmigration mode answers other requests with the 503 "Database migrations are required" response, image endpoint requests are still served. -
#3662
02da2bdThanks @khoinguyenpham04! - Fixes the MCPcontent_listtool suggestingcreated_atandupdated_atfororderBy, which it rejects with a validation error. The tool description now lists only fields it accepts:createdAt,updatedAt,publishedAt,scheduledAt,slug,status,locale, and field slugs that are indexed or set as the collection'stitleFieldordateField. -
#3579
ea3c927Thanks @swissky! - Reduces database queries when rendering navigation menus withgetMenu()andgetMenuWithCacheHint(): a menu and its items now load in a single query, including when the menu falls back to another locale, saving at least one query per menu that isn't already served from the object cache. With an object cache configured, logged-out HTML page loads on Cloudflare D1 and Durable Object databases also skip one more query per request, and menus created or removed by seeding, WordPress import, or site transfer show up without waiting for the cache to expire. -
#3662
02da2bdThanks @khoinguyenpham04! - Fixes content lists skipping entries when paging withnextCursorwhile sorted by a field that can be empty, such as the publish date, scheduled date, title, slug, or a collection'sdateFieldortitleField. This affected the admin content list for collections with more than 100 entries, including its default sort when the collection sets adateField. It also affectedGET /_emdash/api/content/{collection}, the MCPcontent_listtool, and pluginctx.content.listcalls that setorderBy.The order of entries is unchanged, and cursors issued before the upgrade are still accepted.
-
#3662
02da2bdThanks @khoinguyenpham04! - FixesgetEmDashCollectionskipping entries when paging withnextCursorwhile sorted by a field that can be empty, such aspublished_at,title,slug, or a custom date field, including lists filtered by taxonomy terms. Paging by a boolean field or by a system column such asversionalso returns every entry now. The order of entries is unchanged, and cursors issued before the upgrade are still accepted. -
#3651
7583f9bThanks @danielmlr! - Fixes sites usingd1()ordurableObjects()withsessionenabled, orhyperdrive(), reading the preview secret and IP salt from the database on every editor page view, preview link, and comment request.durableObjects()withoutsessiondid the same on write requests, such as comment submissions and reactions. Both values are now read once per isolate, as with other database adapters, so rotating one by deleting itsoptionsrow takes effect after a redeploy. -
#3694
73304f4Thanks @ascorbic! - Speeds up redirect matching on sites with many redirects. Public requests now load the enabled redirect rules in a single query and afterwards check one small row to see whether they changed, instead of reading the whole redirects table every 30 seconds in every Worker isolate. Redirect changes made in the admin, through the API, by automatic slug-change redirects, by seeding or by import take effect as before. A site that has just upgraded, or whose published redirect data is missing or damaged, keeps serving redirects from the redirects table while it rebuilds that data in the background. -
#3692
2a0cb93Thanks @ascorbic! - Fixes enabling a disabled redirect that closes a redirect loop. Enabling it from the admin, the API, or a plugin now fails with a loop validation error, as creating or editing the same redirect already did, and the database rejects it for writers that bypass the API. Previously such an enable succeeded and the loop was only flagged with an admin warning, so automation that toggled a loop-closing redirect on will now receive a validation error instead. Loops that already exist on a site are unchanged and can still be disabled. -
#3693
d1b4402Thanks @ascorbic! - Fixes redirect pattern precedence when several enabled pattern rules match the same path. The earliest-created rule now always wins. Previously the winner depended on database row order, which could change after a rule was edited or hit, particularly on PostgreSQL. -
#3665
1d93e7cThanks @danielmlr! - Fixes the settings page of a plugin installed from the registry or marketplace showing the plugin's internal ID, such asr_dsniqezhchh4zone, as its title instead of its name. The admin endpointGET /_emdash/api/admin/plugins/:idreturns these plugins with the same details as the plugin list instead of a 404. -
#3625
0a5c604Thanks @swissky! - Fixes partialseoandsocialsettings updates through the MCPsettings_updatetool,POST /_emdash/api/settings,setSiteSettings(), and seeds applied withonConflict: "update"replacing the whole stored object. Sending one field, for example{ seo: { googleVerification: "…" } }, used to remove the customrobots.txt, title separator, other verification code, and default social image that were not in the request. Fields you leave out now keep their stored values. To clear a text field insideseoorsocial, send it as an empty string, for example{ social: { twitter: "" } }. -
#3617
7d06f5dThanks @enesismail! - Fixes publishing staged content after one of its fields is deleted. Existing drafts now publish against the current collection schema while their historical revision data remains available. -
#3528
189c6b3Thanks @danielmlr! - Fixes returning visitors seeing a stale page after a content publish until the next deploy. A cached page whoseAstro.cache.set()hints carry no last-modified time no longer sends the build time asLast-Modified, so browsers download the page again instead of receiving a 304. The fix applies to pages that pass no hint and to pages that pass only tag hints, such as those fromgetSiteSettingsWithCacheHint()andgetMenuWithCacheHint()or from a query that returned no entries. Pages whose hints carry a last-modified time, such as those fromgetEmDashEntry(), still revalidate against both the content and the build. -
#3402
f925a89Thanks @danielmlr! - Fixes taxonomy-filteredgetEmDashCollection()listings sorted bypublished_atorcreated_at(the default) on D1 and SQLite reading up to the whole collection when the term has fewer than 200 entries or the listing filters by several terms. These listings now read only the term's entries. A single term with 200 or more entries is still read in date order and stops once the page is full. -
#3608
a40b7ceThanks @danielmlr! - Fixes image, feed and JSON responses for signed-in editors, such as admin media library thumbnails, waiting on the visual editing toolbar. Only HTML pages show the toolbar, so EmDash no longer renders it for other responses. -
#3650
6940899Thanks @swissky! - Fixes pages that answer a missing entry withAstro.rewrite("/404"): the visual editing toolbar (and the client toolbar script) now appears once instead of twice, and the 404 response is kept out of the route cache, so the URL starts working as soon as the entry is published. -
#3622
0b9426eThanks @swissky! - Fixes comment Turnstile verification ignoringEMDASH_TURNSTILE_SECRET_KEYandTURNSTILE_SECRET_KEYwhen they are set at runtime, for example withwrangler secret putor container environment variables. Comment submissions were accepted without a Turnstile check unless the key was also present when the site was built, and a key present at build time was written into the server bundle.On Node, the key must now be in the server's process environment at runtime. If you only set it in a
.envfile, load it when starting the server (for examplenode --env-file=.env ./dist/server/entry.mjs) or set it in your host's environment; otherwise comments are accepted without a Turnstile check. If your server build output was shared or stored, rotate a key that was present at build time. -
Updated dependencies [
75de9a4,85ab50c,3a00448,aa2f87e,76b06d4,a880323,5346dc8,2288fa6,90d71f9,253b6f9,fc90e27,5de3bdc,e811952,7f4064c,998ce63,fc019e4,1942b3a]:- @emdash-cms/admin@1.1.0
- @emdash-cms/blocks@1.1.0
- @emdash-cms/auth@1.1.0
- @emdash-cms/gutenberg-to-portable-text@1.1.0