@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 +1 -1
- package/dist/module.mjs +9 -0
- package/dist/runtime/assets/icon.ico +0 -0
- package/dist/runtime/assets/logo.png +0 -0
- package/dist/runtime/assets.d.ts +9 -0
- package/dist/runtime/components/editor/EponymePreviewPanel.vue +4 -3
- package/dist/runtime/composables/useEponyme.d.ts +22 -14
- package/dist/runtime/composables/useEponyme.js +5 -3
- package/dist/runtime/composables/useEponymeCollection.js +6 -4
- package/dist/runtime/composables/useEponymeFavicon.d.ts +1 -0
- package/dist/runtime/composables/useEponymeFavicon.js +14 -0
- package/dist/runtime/pages/EponymeChangePasswordPage.vue +2 -0
- package/dist/runtime/pages/EponymeDetailPage.vue +2 -0
- package/dist/runtime/pages/EponymeIndexPage.vue +2 -0
- package/dist/runtime/pages/EponymeLoginPage.vue +9 -4
- package/dist/runtime/pages/EponymeUsersPage.vue +2 -0
- package/dist/runtime/server/middleware/eponyme-no-store.js +4 -1
- package/dist/runtime/server/utils/eponyme-cache.d.ts +2 -14
- package/dist/runtime/server/utils/eponyme-cache.js +1 -6
- package/dist/runtime/utils/cache-tags.d.ts +46 -0
- package/dist/runtime/utils/cache-tags.js +28 -0
- package/dist/runtime/utils/hydration-cache.d.ts +10 -0
- package/dist/runtime/utils/hydration-cache.js +3 -0
- package/dist/runtime/utils/preview.d.ts +10 -0
- package/dist/runtime/utils/preview.js +7 -2
- package/package.json +1 -1
package/dist/module.json
CHANGED
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
|
|
@@ -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(
|
|
29
|
-
url.searchParams.set(
|
|
30
|
-
url.searchParams.set(
|
|
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
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
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
|
|
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:
|
|
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 {
|
|
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:
|
|
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:
|
|
46
|
+
cache: isPublicContent ? void 0 : "no-store"
|
|
45
47
|
}),
|
|
46
|
-
{ getCachedData:
|
|
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;
|
|
@@ -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
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
|
@@ -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
|
|
28
|
-
const
|
|
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
|
}
|