@karibsen/eponyme 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@karibsen/eponyme",
3
3
  "configKey": "eponyme",
4
- "version": "0.5.0",
4
+ "version": "0.6.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -2,6 +2,7 @@ import { defineNuxtModule, useLogger, createResolver, findPath, addImports, addI
2
2
  import pc from 'picocolors';
3
3
  import tailwindcss from '@tailwindcss/vite';
4
4
  import { resolve } from 'node:path';
5
+ import { tagPreviewPathRoutes } from '../dist/runtime/utils/cache-tags.js';
5
6
 
6
7
  const module$1 = defineNuxtModule({
7
8
  meta: {
@@ -78,6 +79,14 @@ const module$1 = defineNuxtModule({
78
79
  browserCacheSeconds: options.browserCacheSeconds ?? 30,
79
80
  cdnCacheSeconds: options.cdnCacheSeconds ?? 300
80
81
  };
82
+ const tagged = tagPreviewPathRoutes(options.previewPaths ?? {}, nuxt.options.routeRules ??= {});
83
+ if (tagged.length) {
84
+ const width = Math.max(...tagged.map(({ route }) => route.length));
85
+ logger.info(
86
+ `Cache tags added to ${tagged.length} public route${tagged.length > 1 ? "s" : ""}, so publishing can purge them:
87
+ ` + tagged.map(({ route, tag }) => ` ${route.padEnd(width)} ${tag}`).join("\n")
88
+ );
89
+ }
81
90
  const preparing = Boolean(nuxt.options._prepare);
82
91
  if (!options.prismaClient) {
83
92
  if (!preparing) throw new Error(`${pc.red("[Eponyme]")} eponyme.prismaClient is required.`);
Binary file
Binary file
@@ -0,0 +1,9 @@
1
+ declare module '*.ico?url' {
2
+ const url: string
3
+ export default url
4
+ }
5
+
6
+ declare module '*.png?url' {
7
+ const url: string
8
+ export default url
9
+ }
@@ -3,6 +3,7 @@ import { computed, ref, watch } from "vue";
3
3
  import EPButton from "../ui/EPButton.vue";
4
4
  import EPSelect from "../ui/EPSelect.vue";
5
5
  import { formatVersionDate, useEponymeHistory } from "../../composables/useEponymeHistory";
6
+ import { EPONYME_PREVIEW_QUERY, EPONYME_PREVIEW_TOKEN_QUERY, EPONYME_PREVIEW_VERSION_QUERY } from "../../utils/preview";
6
7
  const props = defineProps({
7
8
  name: { type: String, required: true },
8
9
  path: { type: String, required: true },
@@ -25,9 +26,9 @@ const options = computed(() => [
25
26
  ]);
26
27
  const previewUrl = computed(() => {
27
28
  const url = new URL(props.path, "http://placeholder");
28
- url.searchParams.set("__eponyme_preview", props.name);
29
- url.searchParams.set("__eponyme_preview_version", selected.value);
30
- url.searchParams.set("__eponyme_preview_token", previewToken.value);
29
+ url.searchParams.set(EPONYME_PREVIEW_QUERY, props.name);
30
+ url.searchParams.set(EPONYME_PREVIEW_VERSION_QUERY, selected.value);
31
+ url.searchParams.set(EPONYME_PREVIEW_TOKEN_QUERY, previewToken.value);
31
32
  return `${url.pathname}${url.search}`;
32
33
  });
33
34
  async function reload() {
@@ -1,4 +1,4 @@
1
- import type { Ref } from 'vue';
1
+ import type { ComputedRef, Ref } from 'vue';
2
2
  import type eponymeConfig from '#eponyme/config';
3
3
  import type { EponymeDataByName, EponymeName } from '../types/index.js';
4
4
  import type { EponymeAction, EponymeStatus, EponymeVersionSelector } from '../server/services/eponyme-store.js';
@@ -9,18 +9,26 @@ export interface UseEponymeOptions {
9
9
  /** Keep `{{ variables }}` unresolved. The dashboard editor needs the source text. */
10
10
  raw?: boolean;
11
11
  }
12
- /** Read and save a configured eponyme through the public or draft API. */
13
- export declare function useEponyme<const Name extends ConfigEponymeName>(name: Name, options?: UseEponymeOptions): {
14
- data: Ref<EponymeDataByName<any, Name> | undefined>;
15
- status: import("vue").ComputedRef<EponymeStatus>;
16
- publishedAt: import("vue").ComputedRef<string | null>;
17
- pending: import("vue").ComputedRef<boolean>;
18
- error: Ref<import("nuxt/app").NuxtError<unknown> | undefined, import("nuxt/app").NuxtError<unknown> | undefined>;
19
- errors: Ref<ValidationErrors, ValidationErrors>;
12
+ /**
13
+ * Named, and annotated on the function below, rather than left to inference.
14
+ *
15
+ * An inferred return type has to be expanded when the declaration files are generated, and
16
+ * the expansion loses `typeof eponymeConfig`: it came out as `EponymeDataByName<any, Name>`,
17
+ * which collapses every field to the union of all possible field values. Source and
18
+ * playground were unaffected — they read `src` — so only installed versions of the package
19
+ * lost their types, on the one composable everything else goes through.
20
+ */
21
+ export interface UseEponymeResult<Data extends Record<string, unknown>> {
22
+ data: Ref<Data | undefined>;
23
+ status: ComputedRef<EponymeStatus>;
24
+ publishedAt: ComputedRef<string | null>;
25
+ pending: ComputedRef<boolean>;
26
+ error: Ref<Error | null | undefined>;
27
+ errors: Ref<ValidationErrors>;
20
28
  refresh: () => Promise<void>;
21
- save: {
22
- (): Promise<EponymeDataByName<any, Name> | undefined>;
23
- (patch: Partial<EponymeDataByName<any, Name>>, action?: EponymeAction): Promise<EponymeDataByName<any, Name> | undefined>;
24
- };
25
- };
29
+ /** Without a patch, saves the data currently held. Publishes unless told otherwise. */
30
+ save: (patch?: Partial<Data>, action?: EponymeAction) => Promise<Data | undefined>;
31
+ }
32
+ /** Read and save a configured eponyme through the public or draft API. */
33
+ export declare function useEponyme<const Name extends ConfigEponymeName>(name: Name, options?: UseEponymeOptions): UseEponymeResult<EponymeDataByName<typeof eponymeConfig, Name>>;
26
34
  export {};
@@ -1,7 +1,7 @@
1
1
  import { clearNuxtData, useAsyncData, useRequestFetch, useRoute } from "#app";
2
2
  import { computed, ref } from "vue";
3
3
  import { readPreviewQuery, readPreviewVersion } from "../utils/preview.js";
4
- import { cacheDuringHydrationOnly } from "../utils/hydration-cache.js";
4
+ import { cacheForPublicRead } from "../utils/hydration-cache.js";
5
5
  export function useEponyme(name, options = {}) {
6
6
  const route = useRoute();
7
7
  const preview = readPreviewQuery(route.query);
@@ -10,11 +10,13 @@ export function useEponyme(name, options = {}) {
10
10
  const saving = ref(false);
11
11
  const requestFetch = useRequestFetch();
12
12
  const cacheKey = `eponyme:${name}:${version}${options.raw ? ":raw" : ""}`;
13
- const fetchCache = version === "published" && !options.raw ? void 0 : "no-store";
13
+ const isPublicContent = version === "published" && !options.raw;
14
+ const fetchCache = isPublicContent ? void 0 : "no-store";
15
+ const isPreviewRead = options.version === void 0 && !isPublicContent;
14
16
  const { data: response, pending: loading, error, refresh: load } = useAsyncData(
15
17
  cacheKey,
16
18
  () => requestFetch(`/api/eponyme/${name}`, { query: { version, raw: options.raw ? 1 : void 0 }, cache: fetchCache }),
17
- { getCachedData: cacheDuringHydrationOnly }
19
+ { server: !isPreviewRead, getCachedData: cacheForPublicRead(isPublicContent) }
18
20
  );
19
21
  const data = computed(() => response.value?.data);
20
22
  const status = computed(() => response.value?.status ?? "published");
@@ -2,7 +2,7 @@ import { useAsyncData, useRequestFetch, useRoute } from "#app";
2
2
  import { useEventListener } from "@vueuse/core";
3
3
  import { computed } from "vue";
4
4
  import { readPreviewQuery, readPreviewVersion } from "../utils/preview.js";
5
- import { cacheDuringHydrationOnly } from "../utils/hydration-cache.js";
5
+ import { cacheForPublicRead } from "../utils/hydration-cache.js";
6
6
  export function useEponymeCollection(name, options = {}) {
7
7
  const requestFetch = useRequestFetch();
8
8
  const query = {
@@ -17,7 +17,7 @@ export function useEponymeCollection(name, options = {}) {
17
17
  `eponyme:collection:public:${name}:${options.take ?? ""}:${options.skip ?? ""}:${options.orderBy ?? ""}:${options.order ?? ""}`,
18
18
  // Always the published listing, which is publicly cacheable: the browser cache may answer it.
19
19
  () => requestFetch(`/api/eponyme-collections/${name}`, { query }),
20
- { getCachedData: cacheDuringHydrationOnly }
20
+ { getCachedData: cacheForPublicRead(true) }
21
21
  );
22
22
  const entries = computed(() => result.data.value?.entries ?? []);
23
23
  if (import.meta.client) useEventListener(window, "focus", () => void result.refresh());
@@ -36,14 +36,16 @@ export function useEponymeCollectionEntry(name, slug, options = {}) {
36
36
  const preview = readPreviewQuery(route.query);
37
37
  const version = options.version ?? (preview.entry === `${name}/${slug}` ? readPreviewVersion(preview.version) : "published");
38
38
  const requestFetch = useRequestFetch();
39
+ const isPublicContent = version === "published";
40
+ const isPreviewRead = options.version === void 0 && !isPublicContent;
39
41
  const result = useAsyncData(
40
42
  `eponyme:collection-entry:${name}:${slug}:${version}`,
41
43
  () => requestFetch(`/api/eponyme/${name}/${encodeURIComponent(slug)}`, {
42
44
  query: { version },
43
45
  // A draft or a historical version must never be stored, whatever the server says.
44
- cache: version === "published" ? void 0 : "no-store"
46
+ cache: isPublicContent ? void 0 : "no-store"
45
47
  }),
46
- { getCachedData: cacheDuringHydrationOnly }
48
+ { server: !isPreviewRead, getCachedData: cacheForPublicRead(isPublicContent) }
47
49
  );
48
50
  if (import.meta.client) useEventListener(window, "focus", () => void result.refresh());
49
51
  return {
@@ -0,0 +1 @@
1
+ export declare function useEponymeFavicon(): void;
@@ -0,0 +1,14 @@
1
+ import { useHead } from "#app";
2
+ import iconUrl from "../assets/icon.ico?url";
3
+ export function useEponymeFavicon() {
4
+ useHead({
5
+ link: [
6
+ {
7
+ key: "eponyme-favicon",
8
+ rel: "icon",
9
+ type: "image/x-icon",
10
+ href: iconUrl
11
+ }
12
+ ]
13
+ });
14
+ }
@@ -2,9 +2,11 @@
2
2
  import { navigateTo, useRuntimeConfig, useSeoMeta } from "#app";
3
3
  import { computed, ref } from "vue";
4
4
  import { useEponymeAuth } from "../composables/useEponymeAuth";
5
+ import { useEponymeFavicon } from "../composables/useEponymeFavicon";
5
6
  import EPButton from "../components/ui/EPButton.vue";
6
7
  import EPFormField from "../components/ui/EPFormField.vue";
7
8
  import EPInputText from "../components/ui/EPInputText.vue";
9
+ useEponymeFavicon();
8
10
  useSeoMeta({ title: "Change password \xB7 Eponyme" });
9
11
  const auth = useEponymeAuth();
10
12
  const currentPassword = ref("");
@@ -9,6 +9,7 @@ import EponymeFolderPage from "../components/editor/EponymeFolderPage.vue";
9
9
  import EponymePageNavigation from "../components/editor/EponymePageNavigation.vue";
10
10
  import EponymeSidebar from "../components/editor/EponymeSidebar.vue";
11
11
  import { useEponymeConfig } from "../composables/useEponymeConfig";
12
+ import { useEponymeFavicon } from "../composables/useEponymeFavicon";
12
13
  import { getEponymeCollections, getEponymeForms, getEponymeSchemas } from "../utils/get-eponyme-schemas";
13
14
  const route = useRoute();
14
15
  const config = useEponymeConfig();
@@ -29,6 +30,7 @@ const isFolder = computed(() => !schemas[eponymeName.value] && !collection.value
29
30
  function label(name) {
30
31
  return name.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
31
32
  }
33
+ useEponymeFavicon();
32
34
  useSeoMeta({
33
35
  title: () => `${label(eponymeName.value.split("/").at(-1) ?? eponymeName.value)} \xB7 Eponyme`
34
36
  });
@@ -8,6 +8,7 @@ import EPDialog from "../components/ui/EPDialog.vue";
8
8
  import EPToast from "../components/ui/EPToast.vue";
9
9
  import { useEponymeAuth } from "../composables/useEponymeAuth";
10
10
  import { useEponymeConfig } from "../composables/useEponymeConfig";
11
+ import { useEponymeFavicon } from "../composables/useEponymeFavicon";
11
12
  import { getEponymeCollections, getEponymeForms, getEponymeSchemas } from "../utils/get-eponyme-schemas";
12
13
  const config = useEponymeConfig();
13
14
  const route = useRoute();
@@ -17,6 +18,7 @@ const eponymes = getEponymeSchemas(config);
17
18
  const collections = getEponymeCollections(config);
18
19
  const forms = getEponymeForms(config);
19
20
  const entries = { ...eponymes, ...collections, ...forms };
21
+ useEponymeFavicon();
20
22
  useSeoMeta({ title: "Eponyme" });
21
23
  const fileInput = ref();
22
24
  const exporting = ref(false);
@@ -2,15 +2,18 @@
2
2
  import { navigateTo, useRoute, useRuntimeConfig, useSeoMeta } from "#app";
3
3
  import { computed, ref } from "vue";
4
4
  import { useEponymeAuth } from "../composables/useEponymeAuth";
5
+ import { useEponymeFavicon } from "../composables/useEponymeFavicon";
5
6
  import EPButton from "../components/ui/EPButton.vue";
6
7
  import EPFormField from "../components/ui/EPFormField.vue";
7
8
  import EPInputText from "../components/ui/EPInputText.vue";
9
+ import logoUrl from "../assets/logo.png?url";
8
10
  const route = useRoute();
9
11
  const auth = useEponymeAuth();
10
12
  const username = ref("");
11
13
  const password = ref("");
12
14
  const error = ref("");
13
15
  const dashboardPath = computed(() => useRuntimeConfig().public.eponyme?.dashboardPath ?? "/__eponyme");
16
+ useEponymeFavicon();
14
17
  useSeoMeta({ title: "Sign in \xB7 Eponyme" });
15
18
  const existingUser = await auth.refresh();
16
19
  if (existingUser)
@@ -37,10 +40,12 @@ async function navigateAfterLogin(mustChangePassword) {
37
40
  <template>
38
41
  <main class="eponyme-root ep:flex ep:min-h-screen ep:items-center ep:justify-center ep:bg-theme-ep ep:px-6 ep:py-12 ep:font-sans ep:text-text-ep">
39
42
  <section class="ep:w-full ep:max-w-sm">
40
- <p class="ep:m-0 ep:text-[11px] ep:font-semibold ep:tracking-widest ep:text-muted-ep ep:uppercase">
41
- Eponyme
42
- </p>
43
- <h1 class="ep:mt-2 ep:mb-2 ep:text-3xl ep:font-semibold ep:tracking-tight ep:text-white">
43
+ <img
44
+ :src="logoUrl"
45
+ alt="Eponyme"
46
+ class="ep:block ep:size-14"
47
+ >
48
+ <h1 class="ep:mt-4 ep:mb-2 ep:text-3xl ep:font-semibold ep:tracking-tight ep:text-white">
44
49
  Sign in
45
50
  </h1>
46
51
  <p class="ep:mt-0 ep:mb-8 ep:text-sm ep:leading-relaxed ep:text-muted-ep">
@@ -3,6 +3,7 @@ import { useRequestFetch, useRoute, useSeoMeta } from "#app";
3
3
  import { computed, ref } from "vue";
4
4
  import EponymeCopy from "../components/editor/EponymeCopy.vue";
5
5
  import EponymeSidebar from "../components/editor/EponymeSidebar.vue";
6
+ import { useEponymeFavicon } from "../composables/useEponymeFavicon";
6
7
  import EPButton from "../components/ui/EPButton.vue";
7
8
  import EPDialog from "../components/ui/EPDialog.vue";
8
9
  import EPFormField from "../components/ui/EPFormField.vue";
@@ -11,6 +12,7 @@ import EPSelect from "../components/ui/EPSelect.vue";
11
12
  import EPSwitch from "../components/ui/EPSwitch.vue";
12
13
  import EPTooltip from "../components/ui/EPTooltip.vue";
13
14
  import EPAvatar from "../components/ui/EPAvatar.vue";
15
+ useEponymeFavicon();
14
16
  useSeoMeta({ title: "Users \xB7 Eponyme" });
15
17
  const route = useRoute();
16
18
  const requestFetch = useRequestFetch();
@@ -1,6 +1,9 @@
1
1
  import { defineEventHandler, getRequestURL } from "h3";
2
2
  import { setEponymePrivateCache } from "../utils/eponyme-cache.js";
3
+ import { EPONYME_PREVIEW_QUERY } from "../../utils/preview.js";
3
4
  export default defineEventHandler((event) => {
4
- if (!getRequestURL(event).pathname.startsWith("/api/eponyme")) return;
5
+ const url = getRequestURL(event);
6
+ if (url.searchParams.has(EPONYME_PREVIEW_QUERY)) return setEponymePrivateCache(event);
7
+ if (!url.pathname.startsWith("/api/eponyme")) return;
5
8
  setEponymePrivateCache(event);
6
9
  });
@@ -1,18 +1,6 @@
1
1
  import type { H3Event } from 'h3';
2
- /**
3
- * Cache tags for one entry, most specific first.
4
- *
5
- * A CDN cannot usually be purged by URL — Vercel and Cloudflare both purge by tag — and one
6
- * entry appears in more than one cached response: its own, and its collection's listing.
7
- * Tagging both is what lets publishing an article drop the article *and* the index that
8
- * lists it.
9
- *
10
- * A comma is the delimiter every CDN uses for this header, so it is stripped rather than
11
- * escaped: a tag containing one would silently become two.
12
- */
13
- export declare function getEponymeCacheTags(name: string, collection?: {
14
- name: string;
15
- } | string): string[];
2
+ /** Re-exported so server code keeps one import for everything cache-related. */
3
+ export { getEponymeCacheTags } from '../../utils/cache-tags.js';
16
4
  /**
17
5
  * Published content is identical for every visitor, so both the browser and the CDN may
18
6
  * hold on to it. The two windows are deliberately different:
@@ -1,12 +1,7 @@
1
1
  import { setResponseHeader } from "h3";
2
2
  import { useRuntimeConfig } from "nitropack/runtime";
3
3
  const STALE_WHILE_REVALIDATE_FACTOR = 12;
4
- export function getEponymeCacheTags(name, collection) {
5
- const collectionName = typeof collection === "string" ? collection : collection?.name;
6
- const tags = ["eponyme", `eponyme:${name}`];
7
- if (collectionName) tags.push(`eponyme:${collectionName}`);
8
- return [...new Set(tags)].map((tag) => tag.replace(/,/g, "").slice(0, 256));
9
- }
4
+ export { getEponymeCacheTags } from "../../utils/cache-tags.js";
10
5
  function setCacheTags(event, tags) {
11
6
  if (!tags.length) return;
12
7
  const value = tags.join(",");
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Single source of truth for Eponyme cache tags, shared by the API responses that carry
3
+ * them, the module that tags the host's page routes at build time, and any purge the host
4
+ * runs on publication. Kept free of server imports so `module.ts` can use it too: a page
5
+ * and its API response must be tagged by the same code, or a purge would drop one and
6
+ * leave the other.
7
+ *
8
+ * A CDN cannot usually be purged by URL — Vercel and Cloudflare both purge by tag — and one
9
+ * entry appears in more than one cached response: its own, its collection's listing, and the
10
+ * page that renders it. Tagging all of them is what lets publishing an article drop the
11
+ * article, the index that lists it, and the HTML a visitor would otherwise keep receiving.
12
+ *
13
+ * A comma is the delimiter every CDN uses for this header, so it is stripped rather than
14
+ * escaped: a tag containing one would silently become two.
15
+ */
16
+ export declare function getEponymeCacheTags(name: string, collection?: {
17
+ name: string;
18
+ } | string): string[];
19
+ /** The shape of `nuxt.options.routeRules` this needs, declared so this file stays build-safe. */
20
+ type TaggableRouteRules = Record<string, {
21
+ headers?: Record<string, string>;
22
+ }>;
23
+ /**
24
+ * Tags the host's public routes with the same tags their API responses carry.
25
+ *
26
+ * A published entry ends up in two caches: the JSON the browser reads, and — as soon as the
27
+ * host puts a route under `swr`, `isr` or `prerender` — the HTML itself. Only the first was
28
+ * tagged, so a purge on publication dropped the JSON and left visitors on the old page.
29
+ * `previewPaths` already declares which route renders which entry, which is exactly the
30
+ * mapping needed to tag the other one, once, at build time.
31
+ *
32
+ * A collection pattern cannot name a slug in a route rule, so its pages carry the
33
+ * collection-level tag: publishing one article purges every article page and the index that
34
+ * lists them. Broader than strictly needed, and never wrong — tagging per entry would mean
35
+ * tracking every entry read during a render, and nitro's request context is not shared
36
+ * across the internal fetch that reads them.
37
+ *
38
+ * A header the host wrote wins: this only fills in what nobody set. Returns what it did, so
39
+ * the module can say it out loud: writing into someone else's `routeRules` is invisible to
40
+ * whoever reads their own config, and an effect nobody can see is one nobody can debug.
41
+ */
42
+ export declare function tagPreviewPathRoutes(previewPaths: Record<string, string>, routeRules: TaggableRouteRules): {
43
+ route: string;
44
+ tag: string;
45
+ }[];
46
+ export {};
@@ -0,0 +1,28 @@
1
+ export function getEponymeCacheTags(name, collection) {
2
+ const collectionName = typeof collection === "string" ? collection : collection?.name;
3
+ const tags = ["eponyme", `eponyme:${name}`];
4
+ if (collectionName) tags.push(`eponyme:${collectionName}`);
5
+ return [...new Set(tags)].map((tag) => tag.replace(/,/g, "").slice(0, 256));
6
+ }
7
+ export function tagPreviewPathRoutes(previewPaths, routeRules) {
8
+ const tagged = [];
9
+ for (const [name, path] of Object.entries(previewPaths)) {
10
+ if (!path.startsWith("/")) continue;
11
+ const isCollection = path.includes(":slug");
12
+ const route = isCollection ? path.replace(/:slug\b.*$/, "**") : path;
13
+ const tags = getEponymeCacheTags(name, isCollection ? name : void 0);
14
+ const value = tags.join(",");
15
+ const existing = routeRules[route] ?? {};
16
+ routeRules[route] = {
17
+ ...existing,
18
+ headers: {
19
+ // Vercel reads its own header; `Cache-Tag` is what Cloudflare and Fastly-style CDNs read.
20
+ "Vercel-Cache-Tag": value,
21
+ "Cache-Tag": value,
22
+ ...existing.headers
23
+ }
24
+ };
25
+ tagged.push({ route, tag: tags[tags.length - 1] });
26
+ }
27
+ return tagged;
28
+ }
@@ -9,3 +9,13 @@ import type { NuxtApp } from '#app';
9
9
  * first paint faithful and still refetches on every later navigation.
10
10
  */
11
11
  export declare function cacheDuringHydrationOnly<T>(key: string, nuxtApp: NuxtApp): T | undefined;
12
+ /**
13
+ * `getCachedData` for one read, or `undefined` to keep Nuxt's own policy.
14
+ *
15
+ * Published content is the same for every visitor and already answers with a public
16
+ * `Cache-Control`, so Nuxt's default is exactly right: the SSR payload while hydrating,
17
+ * then `nuxtApp.static.data`, which prerendered and prefetched payloads fill. A client
18
+ * navigation to a prerendered route then costs no request at all. Unreleased material
19
+ * keeps refetching after hydration.
20
+ */
21
+ export declare function cacheForPublicRead(isPublic: boolean): typeof cacheDuringHydrationOnly | undefined;
@@ -1,3 +1,6 @@
1
1
  export function cacheDuringHydrationOnly(key, nuxtApp) {
2
2
  return nuxtApp.isHydrating ? nuxtApp.payload.data[key] : void 0;
3
3
  }
4
+ export function cacheForPublicRead(isPublic) {
5
+ return isPublic ? void 0 : cacheDuringHydrationOnly;
6
+ }
@@ -20,6 +20,16 @@ export declare function splitCollectionEntry(collectionNames: string[], name: st
20
20
  * returns `previewPaths[collection]` with `:slug` filled in.
21
21
  */
22
22
  export declare function resolvePreviewPath(previewPaths: Record<string, string>, collectionNames: string[], name: string): string | undefined;
23
+ /**
24
+ * The query parameters that turn a public route into a preview.
25
+ *
26
+ * Named here because three places have to agree on them: the panel that builds the URL,
27
+ * the composables that read it, and the middleware that keeps such a response out of
28
+ * every cache. A typo in any one of them fails silently, in the direction of leaking.
29
+ */
30
+ export declare const EPONYME_PREVIEW_QUERY = "__eponyme_preview";
31
+ export declare const EPONYME_PREVIEW_VERSION_QUERY = "__eponyme_preview_version";
32
+ export declare const EPONYME_PREVIEW_TOKEN_QUERY = "__eponyme_preview_token";
23
33
  /** Reads `?__eponyme_preview_version=` — `draft` by default, or a numeric history id. */
24
34
  export declare function readPreviewVersion(raw: unknown): PreviewVersion;
25
35
  /** Reads the preview query params, unwrapping the repeated-param array form. */
@@ -18,13 +18,18 @@ export function resolvePreviewPath(previewPaths, collectionNames, name) {
18
18
  if (!pattern || !pattern.includes(":slug")) return void 0;
19
19
  return applyPreviewSlug(pattern, entry.slug);
20
20
  }
21
+ export const EPONYME_PREVIEW_QUERY = "__eponyme_preview";
22
+ export const EPONYME_PREVIEW_VERSION_QUERY = "__eponyme_preview_version";
23
+ export const EPONYME_PREVIEW_TOKEN_QUERY = "__eponyme_preview_token";
21
24
  export function readPreviewVersion(raw) {
22
25
  if (raw === "published") return "published";
23
26
  const id = Number(raw);
24
27
  return Number.isSafeInteger(id) && id > 0 ? id : "draft";
25
28
  }
26
29
  export function readPreviewQuery(query) {
27
- const entry = Array.isArray(query.__eponyme_preview) ? query.__eponyme_preview[0] : query.__eponyme_preview;
28
- const version = Array.isArray(query.__eponyme_preview_version) ? query.__eponyme_preview_version[0] : query.__eponyme_preview_version;
30
+ const rawEntry = query[EPONYME_PREVIEW_QUERY];
31
+ const rawVersion = query[EPONYME_PREVIEW_VERSION_QUERY];
32
+ const entry = Array.isArray(rawEntry) ? rawEntry[0] : rawEntry;
33
+ const version = Array.isArray(rawVersion) ? rawVersion[0] : rawVersion;
29
34
  return { entry: entry === void 0 || entry === null ? void 0 : String(entry), version };
30
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karibsen/eponyme",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "A lightweight, type-safe content manager for Nuxt.",
5
5
  "license": "MIT",
6
6
  "type": "module",