Releases: karibsen-studio/eponyme
Release list
v0.7.0
🚀 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
urlandemailflags from text field options — breaking (e7eedfc) - Remove captcha support and its adapter — breaking (cd17e74)
- Warn at build time when
@nuxt/uiis older than4.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
middleEllipsisinto a shared util (8dd9e76) - Drop redundant classes on the submissions table header (c83f21e)
📦 Build
📚 Documentation
- Document
ModuleOptionswith its defaults and examples (9b3f047) - Add a cover image to the readme (5922b96)
❤️ Contributors
- Corentin Nelhomme (
@d3ller)
v0.5.0
🏷️ 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-TagandCache-Tagon 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-storeresponse 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-storeroute carries none
Full Changelog: v0.4.0...v0.5.0
v0.4.0
⚡ 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=1reads 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:
browserCacheSecondsandcdnCacheSecondsoptions
✅ Tests
- The cache boundary is asserted end to end: published routes are
public, and drafts,rawreads, history, trash, submissions, users and the session route areno-store, authenticated or not
Full Changelog: v0.3.0...v0.4.0
v0.3.0
⚡ 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 —
listCollectionre-read each row throughgetResulteven thoughfindManyhad already returned its payload - store: Reading a singleton no longer writes — the per-read
upserttook 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,0to 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
updatedAtthey 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:
cacheSecondsoption
✅ 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
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 writingAlso 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.