github emdash-cms/emdash emdash@0.6.0

latest release: @emdash-cms/x402@0.6.0
9 hours ago

Minor Changes

  • #626 1859347 Thanks @ascorbic! - Adds eager hydration of taxonomy terms on getEmDashCollection and getEmDashEntry results. Each entry now exposes a data.terms field 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 called getEntryTerms(collection, id, taxonomy) per entry can read entry.data.terms directly and skip the N+1 round-trip.

    New exports: getAllTermsForEntries, invalidateTermCache.

    Reserved field slugs now also block terms, bylines, and byline at 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 of bylines / byline hydration); rename the field to keep its data accessible.

  • #600 9295cc1 Thanks @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 new fonts.scripts config option. Set fonts: false to disable and use system fonts.

Patch Changes

  • #648 ada4ac7 Thanks @CacheMeOwside! - Adds the missing url field type for seed files, content type builder, and content editor with client-side URL validation.

  • #658 f279320 Thanks @ascorbic! - Adds after(fn) — a helper for deferring bookkeeping work past the HTTP response. On Cloudflare it hands off to waitUntil (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 new virtual:emdash/wait-until virtual 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_tasks UPDATE) 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 7f75193 Thanks @Pouf5! - Adds maxUploadSize config option to set the maximum media file upload size in bytes. Defaults to 52_428_800 (50 MB) — existing behaviour is unchanged.

  • #595 cfd01f3 Thanks @ascorbic! - Fixes playground initialization crash caused by syncSearchState attempting first-time FTS enablement during field creation.

  • #663 38d637b Thanks @ascorbic! - Cache getSiteSetting(key) per-request. It was firing an uncached options table read on every call, so templates that pull several settings (or EmDashHead reading seo on 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 31d2f4e Thanks @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 445b3bf Thanks @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/d1 behind a single createRequestScopedDb(opts) function. Core middleware has no D1-specific logic. Adapters opt in via a new supportsRequestScope: boolean flag on DatabaseDescriptor; d1() sets it to true.

    Other fixes in the same change:

    • Nested runWithContext calls 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 !config bail-out now still applies baseline security headers.
    • __ec_d1_bookmark references aligned to __em_d1_bookmark across runtime, docs, and JSDoc.
  • #654 943d540 Thanks @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 globalThis with a Symbol key (same pattern as request-context.ts), so a single value is shared across all chunks now.
    • Wraps getCollectionInfo, getTaxonomyDef, getTaxonomyTerms, and getEmDashCollection in 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.

  • #668 2cb3165 Thanks @CacheMeOwside! - Fixes boolean field checkbox displaying as unchecked after publish in the admin UI.

  • #500 14c923b Thanks @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 c5ef0f5 Thanks @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 f839381 Thanks @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 002d0ac Thanks @ascorbic! - getSiteSetting(key) now transparently piggybacks on getSiteSettings() when the batch has already been loaded in the current request. If a parent template has called getSiteSettings() (which is request-cached), a later getSiteSetting("seo") — from EmDashHead, 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 0a61ef4 Thanks @Pouf5! - Fixes FTS5 tables not being created when a searchable collection is created or updated via the Admin UI.

  • #636 6d41fe1 Thanks @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 uses first-unconstrained routing that's free to land on a replica. The singleton goes through the default binding, which the adapter correctly promotes to first-primary for 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 b158e40 Thanks @ascorbic! - Prime the request-scoped cache for getEntryTerms during collection and entry hydration. getEmDashCollection and getEmDashEntry already 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 keys getEntryTerms uses, so existing templates that still call getEntryTerms(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 f97d6ab Thanks @ascorbic! - Adds opt-in query instrumentation for performance regression testing. Setting EMDASH_QUERY_LOG=1 causes 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 an X-Perf-Phase header 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/instrumentation so first-party adapters (e.g. @emdash-cms/cloudflare) can wire the same hook into their per-request Kysely instances.

  • #613 e67b940 Thanks @nickgraynews! - Fixes site SEO settings googleVerification and bingVerification not 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 0896ec8 Thanks @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 isMissingTableError catch. 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() and invalidateTermCache() are preserved as no-op exports so callers don't break.

  • #558 629fe1d Thanks @csfalcao! - Fixes /_emdash/api/search/suggest 500 error. getSuggestions no longer double-appends the FTS5 prefix operator * on top of the one escapeQuery already adds, so autocomplete queries like ?q=des now return results instead of raising SqliteError: fts5: syntax error near "*".

  • #552 f52154d Thanks @masonjames! - Fixes passkey login failures so unregistered or invalid credentials return an authentication failure instead of an internal server error.

  • #601 8221c2a Thanks @CacheMeOwside! - Fixes the Save Changes button on the Content Type editor failing silently with a 400 error

  • #598 8fb93eb Thanks @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 6d7f288 Thanks @CacheMeOwside! - Adds toast feedback when taxonomy assignments are saved or fail on content items.

  • #638 4ffa141 Thanks @auggernaut! - Fixes repeated FTS startup rebuilds on SQLite by verifying indexed row counts against the FTS shadow table.

  • #582 04e6cca Thanks @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/admin@0.6.0
    • @emdash-cms/auth@0.6.0
    • @emdash-cms/gutenberg-to-portable-text@0.6.0

Don't miss a new emdash release

NewReleases is sending notifications on new releases.