Skip to content

Releases: karibsen-studio/eponyme

v0.7.0

Choose a tag to compare

@D3ller D3ller released this 30 Jul 02:55

🚀 Enhancements

  • Add field.phone(), stored in E.164 and restricted to the countries you accept (72fd194)
  • Let field.url() restrict the protocols an external link may use (e7eedfc)
  • Remove the url and email flags from text field options — breaking (e7eedfc)
  • Remove captcha support and its adapter — breaking (cd17e74)
  • Warn at build time when @nuxt/ui is older than 4.10.0 (ee555f5)
  • Show the Eponyme logo in the dashboard sidebar (ccc9612)
  • Shorten a long href in the link editor (8d8e31f)

🩹 Fixes

  • Fill in the defaults of fields nested in a section, a tab or an array item (c342ce3)

💅 Refactors

  • Extract middleEllipsis into a shared util (8dd9e76)
  • Drop redundant classes on the submissions table header (c83f21e)

📦 Build

  • Add libphonenumber-js (6ab2d38)
  • Ignore the local template and guideline files (675b886)

📚 Documentation

  • Document ModuleOptions with its defaults and examples (9b3f047)
  • Add a cover image to the readme (5922b96)

❤️ Contributors

  • Corentin Nelhomme (@d3ller)

v0.5.0

Choose a tag to compare

@D3ller D3ller released this 29 Jul 19:39

🏷️ CDN purging. Publishing left the CDN serving the previous content until cdnCacheSeconds expired. Cached responses are now tagged, so a purge can name exactly what changed.

A CDN cannot usually be purged by URL — Vercel, Cloudflare and Fastly all purge by tag — and one entry appears in more than one cached response: its own, and the listing of the collection that contains it.

Response Tags
pages/homepage eponyme, eponyme:pages/homepage
articles/my-article eponyme, eponyme:articles/my-article, eponyme:articles
The articles listing eponyme, eponyme:articles

Publishing an article therefore drops both the article page and the index that lists it.

Wiring it up

getEponymeCacheTags() is auto-imported into server code and returns exactly the tags the responses were given, so a listener cannot drift from what it is trying to purge:

// server/plugins/purge.ts
import { invalidateByTag } from '@vercel/functions'

export default defineNitroPlugin((nitroApp) => {
  const purge = async ({ name, collection }) => {
    if (!process.env.VERCEL) return
    await invalidateByTag(getEponymeCacheTags(name, collection)).catch(() => {})
  }
  nitroApp.hooks.hook('eponyme:entry:published', purge)
  nitroApp.hooks.hook('eponyme:entry:trashed', purge)
  nitroApp.hooks.hook('eponyme:entry:untrashed', purge)
})

Invalidating rather than deleting marks the entry stale and revalidates in the background, so no visitor waits on the origin. A failed purge should be swallowed: a publication that reached the database succeeded, and an unreachable CDN must not report it as an error the editor cannot act on. The content still appears when cdnCacheSeconds runs out on its own.

browserCacheSeconds remains out of reach of any purge — a visitor already holding a copy keeps it until it expires, which is why it defaults to 30 seconds.

🚀 Features

  • server: Vercel-Cache-Tag and Cache-Tag on every cacheable response
  • server: getEponymeCacheTags(), auto-imported, returning the tags a purge must invalidate

🩹 Fixes

  • server: Nothing uncacheable is tagged — there would be nothing to purge, and a tag on a no-store response only invites the belief that one exists

✅ Tests

  • The emitted tags are asserted end to end, including that a collection entry carries its collection and that a no-store route carries none

Full Changelog: v0.4.0...v0.5.0

v0.4.0

Choose a tag to compare

@D3ller D3ller released this 29 Jul 19:27

Client-side navigation. Moving between pages cost a full round trip for every entry on the page, even for a page just visited. The composables asked for cache: 'no-store' and the server answered no-cache, so both ends were locked against the browser cache. Published content is identical for every visitor, so it is now cacheable.

Measured on a real site: ~120 ms of blocked navigation per click, down to the browser answering from its own cache.

The CDN window is the one that matters for first-time visitors — with public, the edge answers instead of the function, so nobody pays for the database.

⚙️ Configuration

eponyme: {
  cacheSeconds: 5,          // server instance
  browserCacheSeconds: 30,  // visitor's browser — cannot be purged, keep it short
  cdnCacheSeconds: 300,     // CDN — purge from `eponyme:entry:published`
}

browserCacheSeconds is what makes navigation instant, and the one window nobody can shorten: a visitor holding a copy keeps it until it expires, publication or not. Set it to 0 if a publication must reach everyone immediately.

🔥 Performance

  • api: Published entries, collection listings and the sitemap answer public, max-age, s-maxage, stale-while-revalidate
  • composables: Published reads no longer force no-store, so a client-side navigation is served from the browser cache instead of the network

🔒 Security

  • server: A middleware marks every Eponyme response no-store, and only the published routes override it — a route added later is private until someone decides otherwise, rather than public until someone remembers
  • api: Six routes relied on the absence of a cache header rather than on one — history, statuses, trash, submissions, the user list, and the session route that answers who the caller is. The collections route set none at all, drafts included
  • api: raw=1 reads stay out of the shared cache: published content, but with {{ variables }} left unresolved for the editor, so not what a public page renders

🚀 Features

  • module: browserCacheSeconds and cdnCacheSeconds options

✅ Tests

  • The cache boundary is asserted end to end: published routes are public, and drafts, raw reads, history, trash, submissions, users and the session route are no-store, authenticated or not

Full Changelog: v0.3.0...v0.4.0

v0.3.0

Choose a tag to compare

@D3ller D3ller released this 29 Jul 16:00

Performance release. Rendering a public page issued one query per collection entry and wrote to the database on every read of a singleton. Both are fixed.

On a real site, a page combining a singleton and a listing goes from roughly fifteen database round trips to one or two, and to zero once the cache is warm.

If your queries feel slow, also check that your server function runs in the same region as your database — on a serverless host, a default deployment can put the two on different continents, which adds ~90 ms to every query. No amount of caching recovers that.

👉 Changelog

🔥 Performance

  • store: Reading a collection costs one query instead of N+1 — listCollection re-read each row through getResult even though findMany had already returned its payload
  • store: Reading a singleton no longer writes — the per-read upsert took a row lock and wrote WAL just to serve content
  • store: The sitemap and the public listing share one read of a collection's rows
  • store: Instance-level cache of published reads, cacheSeconds (5 by default, 0 to disable), dropped after every write
  • variables: Interpolation returns branches holding no variable as they are instead of rebuilding them

🩹 Fixes

  • store: A shape drift that never converges no longer writes on every read — a reader persists it once per process, so the storage-envelope migration still lands on first read
  • store: Writers never read the cache, so the updatedAt they lock on always comes from the database and a stale key cannot become a spurious conflict
  • store: Drafts are never cached, so the preview panel still shows a save immediately

🚀 Features

  • module: cacheSeconds option

✅ Tests

  • The fake Prisma client counts the queries a call sends, so a return to N+1 or to writing during a read fails the suite instead of merely being slower

Full Changelog: v0.2.0...v0.3.0

v0.2.0

Choose a tag to compare

@D3ller D3ller released this 29 Jul 13:55

Content export and import

The dashboard overview at /__eponyme now carries Export and Import, so content prepared on one environment can be moved to another instead of being retyped.

Export downloads every singleton and live collection entry with its complete state — draft, published version, status and publication date — plus a fingerprint of the schema each entry was written against. Import applies that file on top of the current content: each entry overwrites its counterpart, entries the file does not mention are untouched, and nothing is ever deleted. A single fingerprint divergence refuses the whole file before the first write and names what diverged. Every imported entry is written to the version history, so an import stays reversible entry by entry.

GET  /api/eponyme-export             # editors and owners
POST /api/eponyme-import             # owners only
POST /api/eponyme-import?dryRun=1    # report the counts without writing

Also in this release

  • Recoverable deletion: a deleted collection entry moves to a trash that keeps its content and its whole version history, with restore and permanent delete
  • A server hook contract with typed payloads
  • A navigation tree behind the dashboard sidebar, with filtering
  • A link dialog, downloadable links and the rich-text editor around them

Removed

The captcha adapter is dropped. A public form is an ordinary POST route, so the host application renders the widget and verifies the token in its own handler.