@devite/nuxt-sanity 2.3.2 → 2.4.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.d.mts +19 -11
- package/dist/module.d.ts +19 -11
- package/dist/module.json +1 -1
- package/dist/module.mjs +67 -17
- package/dist/runtime/client/MinimalSanityClient.js +7 -4
- package/dist/runtime/components/SanityImageAsset.vue +2 -3
- package/dist/runtime/imageProviders/sanity.d.ts +2 -0
- package/dist/runtime/imageProviders/sanity.js +16 -0
- package/dist/runtime/plugins/visual-editing.js +4 -3
- package/dist/runtime/server/routes/cache/asset.d.ts +2 -0
- package/dist/runtime/server/routes/cache/asset.js +60 -0
- package/dist/runtime/server/routes/cache/query.d.ts +4 -0
- package/dist/runtime/server/routes/cache/query.js +61 -0
- package/dist/runtime/server/routes/cache/webhook.d.ts +2 -0
- package/dist/runtime/server/routes/cache/webhook.js +33 -0
- package/dist/runtime/server/utils/resolveSanityImageUrl.d.ts +2 -0
- package/dist/runtime/server/utils/resolveSanityImageUrl.js +74 -0
- package/dist/runtime/server/utils/useSanityClient.js +1 -1
- package/dist/types.d.mts +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +3 -2
package/dist/module.d.mts
CHANGED
|
@@ -5,26 +5,33 @@ import { Slug, ImageAsset, PortableTextBlock } from '@sanity/types';
|
|
|
5
5
|
|
|
6
6
|
type ModuleOptions = ClientConfig & {
|
|
7
7
|
projectId: string;
|
|
8
|
-
/** @default
|
|
8
|
+
/** @default "production" */
|
|
9
9
|
dataset: string;
|
|
10
|
-
/** @default
|
|
11
|
-
minimalClient?: boolean
|
|
10
|
+
/** @default { cachingEnabled: true, cacheBaseUrl: "http://localhost:3000", assetEndpoint: "/_sanity/cache/asset", queryEndpoint: "/_sanity/cache/query", webhookEndpoint: "/_sanity/cache/invalidate" } */
|
|
11
|
+
minimalClient?: boolean | {
|
|
12
|
+
cachingEnabled?: boolean;
|
|
13
|
+
cacheBaseUrl?: string;
|
|
14
|
+
assetEndpoint?: string;
|
|
15
|
+
queryEndpoint?: string;
|
|
16
|
+
webhookEndpoint?: string;
|
|
17
|
+
webhookSecret?: string;
|
|
18
|
+
};
|
|
12
19
|
/** @default true */
|
|
13
20
|
useCdn?: boolean;
|
|
14
|
-
/** @default
|
|
21
|
+
/** @default "2024-08-08" */
|
|
15
22
|
apiVersion?: string;
|
|
16
23
|
visualEditing?: VisualEditingOptions;
|
|
17
24
|
};
|
|
18
25
|
interface VisualEditingOptions {
|
|
19
|
-
/** @default {
|
|
26
|
+
/** @default { enableEndpoint: "/_sanity/preview/enable", disableEndpoint: "/_sanity/preview/disable" } */
|
|
20
27
|
previewMode?: boolean | {
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
enableEndpoint?: string;
|
|
29
|
+
disableEndpoint?: string;
|
|
23
30
|
};
|
|
24
31
|
previewModeId?: string;
|
|
25
|
-
/** @default
|
|
26
|
-
mode?:
|
|
27
|
-
/** @default
|
|
32
|
+
/** @default "live-visual-editing" */
|
|
33
|
+
mode?: SanityVisualEditingMode;
|
|
34
|
+
/** @default "/_sanity/fetch" */
|
|
28
35
|
proxyEndpoint?: string;
|
|
29
36
|
token?: string;
|
|
30
37
|
studioUrl?: string;
|
|
@@ -35,6 +42,7 @@ interface VisualEditingOptions {
|
|
|
35
42
|
/** @default 100 */
|
|
36
43
|
zIndex?: VisualEditingOptions$1['zIndex'];
|
|
37
44
|
}
|
|
45
|
+
type SanityVisualEditingMode = 'live-visual-editing' | 'visual-editing' | 'custom';
|
|
38
46
|
type SanityVisualEditingRefreshHandler = (payload: HistoryRefresh, refreshDefault: () => false | Promise<void>) => false | Promise<void>;
|
|
39
47
|
|
|
40
48
|
interface Page {
|
|
@@ -99,4 +107,4 @@ type SanityArray<T> = Array<T & {
|
|
|
99
107
|
|
|
100
108
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
101
109
|
|
|
102
|
-
export { type GlobalSEO, type Home, type LinkExternal, type LinkInternal, type ModuleOptions, type NotFound, type Page, type RichText, type SEO, type SanityArray, type SanityModule, type SanityVisualEditingRefreshHandler, type VisualEditingOptions, _default as default };
|
|
110
|
+
export { type GlobalSEO, type Home, type LinkExternal, type LinkInternal, type ModuleOptions, type NotFound, type Page, type RichText, type SEO, type SanityArray, type SanityModule, type SanityVisualEditingMode, type SanityVisualEditingRefreshHandler, type VisualEditingOptions, _default as default };
|
package/dist/module.d.ts
CHANGED
|
@@ -5,26 +5,33 @@ import { Slug, ImageAsset, PortableTextBlock } from '@sanity/types';
|
|
|
5
5
|
|
|
6
6
|
type ModuleOptions = ClientConfig & {
|
|
7
7
|
projectId: string;
|
|
8
|
-
/** @default
|
|
8
|
+
/** @default "production" */
|
|
9
9
|
dataset: string;
|
|
10
|
-
/** @default
|
|
11
|
-
minimalClient?: boolean
|
|
10
|
+
/** @default { cachingEnabled: true, cacheBaseUrl: "http://localhost:3000", assetEndpoint: "/_sanity/cache/asset", queryEndpoint: "/_sanity/cache/query", webhookEndpoint: "/_sanity/cache/invalidate" } */
|
|
11
|
+
minimalClient?: boolean | {
|
|
12
|
+
cachingEnabled?: boolean;
|
|
13
|
+
cacheBaseUrl?: string;
|
|
14
|
+
assetEndpoint?: string;
|
|
15
|
+
queryEndpoint?: string;
|
|
16
|
+
webhookEndpoint?: string;
|
|
17
|
+
webhookSecret?: string;
|
|
18
|
+
};
|
|
12
19
|
/** @default true */
|
|
13
20
|
useCdn?: boolean;
|
|
14
|
-
/** @default
|
|
21
|
+
/** @default "2024-08-08" */
|
|
15
22
|
apiVersion?: string;
|
|
16
23
|
visualEditing?: VisualEditingOptions;
|
|
17
24
|
};
|
|
18
25
|
interface VisualEditingOptions {
|
|
19
|
-
/** @default {
|
|
26
|
+
/** @default { enableEndpoint: "/_sanity/preview/enable", disableEndpoint: "/_sanity/preview/disable" } */
|
|
20
27
|
previewMode?: boolean | {
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
enableEndpoint?: string;
|
|
29
|
+
disableEndpoint?: string;
|
|
23
30
|
};
|
|
24
31
|
previewModeId?: string;
|
|
25
|
-
/** @default
|
|
26
|
-
mode?:
|
|
27
|
-
/** @default
|
|
32
|
+
/** @default "live-visual-editing" */
|
|
33
|
+
mode?: SanityVisualEditingMode;
|
|
34
|
+
/** @default "/_sanity/fetch" */
|
|
28
35
|
proxyEndpoint?: string;
|
|
29
36
|
token?: string;
|
|
30
37
|
studioUrl?: string;
|
|
@@ -35,6 +42,7 @@ interface VisualEditingOptions {
|
|
|
35
42
|
/** @default 100 */
|
|
36
43
|
zIndex?: VisualEditingOptions$1['zIndex'];
|
|
37
44
|
}
|
|
45
|
+
type SanityVisualEditingMode = 'live-visual-editing' | 'visual-editing' | 'custom';
|
|
38
46
|
type SanityVisualEditingRefreshHandler = (payload: HistoryRefresh, refreshDefault: () => false | Promise<void>) => false | Promise<void>;
|
|
39
47
|
|
|
40
48
|
interface Page {
|
|
@@ -99,4 +107,4 @@ type SanityArray<T> = Array<T & {
|
|
|
99
107
|
|
|
100
108
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
101
109
|
|
|
102
|
-
export { type GlobalSEO, type Home, type LinkExternal, type LinkInternal, type ModuleOptions, type NotFound, type Page, type RichText, type SEO, type SanityArray, type SanityModule, type SanityVisualEditingRefreshHandler, type VisualEditingOptions, _default as default };
|
|
110
|
+
export { type GlobalSEO, type Home, type LinkExternal, type LinkInternal, type ModuleOptions, type NotFound, type Page, type RichText, type SEO, type SanityArray, type SanityModule, type SanityVisualEditingMode, type SanityVisualEditingRefreshHandler, type VisualEditingOptions, _default as default };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,41 +1,47 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
import { defineNuxtModule, createResolver, addPlugin, addServerHandler, addImportsDir, addImports, addComponentsDir } from '@nuxt/kit';
|
|
2
|
+
import { defineNuxtModule, createResolver, addPlugin, addServerHandler, addImportsDir, addImports, addComponentsDir, installModule } from '@nuxt/kit';
|
|
3
3
|
import defu from 'defu';
|
|
4
4
|
|
|
5
5
|
const name = "@devite/nuxt-sanity";
|
|
6
|
-
const version = "2.
|
|
6
|
+
const version = "2.4.0";
|
|
7
7
|
|
|
8
|
+
const CONFIG_KEY = "sanity";
|
|
8
9
|
const module = defineNuxtModule({
|
|
9
10
|
meta: {
|
|
10
11
|
name,
|
|
11
12
|
version,
|
|
12
|
-
configKey:
|
|
13
|
+
configKey: CONFIG_KEY
|
|
13
14
|
},
|
|
14
15
|
defaults: {},
|
|
15
16
|
async setup(options, nuxt) {
|
|
16
17
|
const { resolve } = createResolver(import.meta.url);
|
|
17
|
-
const
|
|
18
|
+
const $config = nuxt.options.runtimeConfig;
|
|
19
|
+
const moduleConfig = {
|
|
18
20
|
projectId: options.projectId,
|
|
19
21
|
dataset: options.dataset || "production",
|
|
20
|
-
minimalClient: options.minimalClient
|
|
22
|
+
minimalClient: options.minimalClient !== false ? defu(options.minimalClient, {
|
|
23
|
+
cachingEnabled: true,
|
|
24
|
+
cacheBaseUrl: "http://localhost:3000",
|
|
25
|
+
assetEndpoint: "/_sanity/cache/asset",
|
|
26
|
+
queryEndpoint: "/_sanity/cache/query",
|
|
27
|
+
webhookEndpoint: "/_sanity/cache/invalidate"
|
|
28
|
+
}) : false,
|
|
21
29
|
useCdn: options.useCdn || true,
|
|
22
30
|
apiVersion: options.apiVersion || "2024-08-08",
|
|
23
31
|
visualEditing: options.visualEditing || null
|
|
24
|
-
}
|
|
32
|
+
};
|
|
25
33
|
nuxt.options.build.transpile.push("@sanity/core-loader");
|
|
26
|
-
if (
|
|
34
|
+
if (moduleConfig.visualEditing) {
|
|
27
35
|
const previewMode = moduleConfig.visualEditing.previewMode !== false;
|
|
28
36
|
const visualEditingConfig = defu(moduleConfig.visualEditing, {
|
|
29
37
|
mode: "live-visual-editing",
|
|
30
38
|
previewMode: previewMode ? defu(moduleConfig.visualEditing.previewMode, {
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
enableEndpoint: "/_sanity/preview/enable",
|
|
40
|
+
disableEndpoint: "/_sanity/preview/disable"
|
|
33
41
|
}) : false,
|
|
34
|
-
previewModeId: previewMode ? crypto.randomBytes(16).toString("hex") : "",
|
|
35
42
|
proxyEndpoint: "/_sanity/fetch",
|
|
36
43
|
stega: true,
|
|
37
|
-
zIndex: 100
|
|
38
|
-
token: ""
|
|
44
|
+
zIndex: 100
|
|
39
45
|
});
|
|
40
46
|
if (!moduleConfig.visualEditing.token?.length)
|
|
41
47
|
console.warn('Visual editing requires a token with "read" access');
|
|
@@ -57,15 +63,15 @@ const module = defineNuxtModule({
|
|
|
57
63
|
]
|
|
58
64
|
});
|
|
59
65
|
addPlugin({ src: resolve("runtime/plugins/visual-editing") });
|
|
60
|
-
if (previewMode) {
|
|
66
|
+
if (typeof visualEditingConfig.previewMode === "object") {
|
|
61
67
|
addServerHandler({
|
|
62
68
|
method: "get",
|
|
63
|
-
route: visualEditingConfig.previewMode.
|
|
69
|
+
route: visualEditingConfig.previewMode.enableEndpoint,
|
|
64
70
|
handler: resolve("runtime/server/routes/preview/enable")
|
|
65
71
|
});
|
|
66
72
|
addServerHandler({
|
|
67
73
|
method: "get",
|
|
68
|
-
route: visualEditingConfig.previewMode.
|
|
74
|
+
route: visualEditingConfig.previewMode.disableEndpoint,
|
|
69
75
|
handler: resolve("runtime/server/routes/preview/disable")
|
|
70
76
|
});
|
|
71
77
|
}
|
|
@@ -76,8 +82,45 @@ const module = defineNuxtModule({
|
|
|
76
82
|
});
|
|
77
83
|
moduleConfig.visualEditing = visualEditingConfig;
|
|
78
84
|
}
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
if (typeof moduleConfig.minimalClient === "object" && moduleConfig.minimalClient.cachingEnabled) {
|
|
86
|
+
nuxt.options.nitro.storage ||= {};
|
|
87
|
+
nuxt.options.nitro.storage.sanityDocumentDeps = {
|
|
88
|
+
driver: "fsLite",
|
|
89
|
+
base: ".tmp/sanityDocumentDeps"
|
|
90
|
+
};
|
|
91
|
+
nuxt.options.nitro.storage.sanityData = {
|
|
92
|
+
driver: "fsLite",
|
|
93
|
+
base: ".tmp/sanityData"
|
|
94
|
+
};
|
|
95
|
+
addServerHandler({
|
|
96
|
+
method: "get",
|
|
97
|
+
route: moduleConfig.minimalClient.assetEndpoint,
|
|
98
|
+
handler: resolve("runtime/server/routes/cache/asset")
|
|
99
|
+
});
|
|
100
|
+
addServerHandler({
|
|
101
|
+
method: "get",
|
|
102
|
+
route: moduleConfig.minimalClient.queryEndpoint,
|
|
103
|
+
handler: resolve("runtime/server/routes/cache/query")
|
|
104
|
+
});
|
|
105
|
+
if (moduleConfig.minimalClient.webhookSecret) {
|
|
106
|
+
addServerHandler({
|
|
107
|
+
method: "delete",
|
|
108
|
+
route: moduleConfig.minimalClient.webhookEndpoint,
|
|
109
|
+
handler: resolve("runtime/server/routes/cache/webhook")
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
console.warn("Webhook secret is required for webhook-based cache invalidation");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
$config.sanity = defu($config.sanity, {
|
|
116
|
+
visualEditing: moduleConfig.visualEditing && {
|
|
117
|
+
...moduleConfig.visualEditing,
|
|
118
|
+
previewModeId: moduleConfig.visualEditing.previewMode ? crypto.randomBytes(16).toString("hex") : "",
|
|
119
|
+
token: moduleConfig.visualEditing.token || ""
|
|
120
|
+
},
|
|
121
|
+
webhookSecret: moduleConfig.minimalClient && moduleConfig.minimalClient.webhookSecret || void 0
|
|
122
|
+
});
|
|
123
|
+
$config.public.sanity = defu($config.public.sanity, {
|
|
81
124
|
projectId: moduleConfig.projectId,
|
|
82
125
|
dataset: moduleConfig.dataset,
|
|
83
126
|
minimalClient: moduleConfig.minimalClient,
|
|
@@ -122,6 +165,13 @@ const module = defineNuxtModule({
|
|
|
122
165
|
prefix: "Sanity",
|
|
123
166
|
pathPrefix: false
|
|
124
167
|
});
|
|
168
|
+
await installModule("@nuxt/image", {
|
|
169
|
+
providers: {
|
|
170
|
+
cachedSanity: {
|
|
171
|
+
provider: resolve("runtime/imageProviders/sanity")
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}, nuxt);
|
|
125
175
|
}
|
|
126
176
|
});
|
|
127
177
|
|
|
@@ -34,11 +34,13 @@ class MinimalSanityClient extends SanityClient {
|
|
|
34
34
|
}
|
|
35
35
|
async fetch(query, params, _options) {
|
|
36
36
|
const perspectiveQueryString = `&perspective=${_options?.perspective || this.config.perspective}`;
|
|
37
|
-
const queryString = this.toQueryString(query, params || {}) + perspectiveQueryString;
|
|
37
|
+
const queryString = this.toQueryString(query, params || {}) + (this.config.useCdn ? "" : perspectiveQueryString);
|
|
38
38
|
const byteLength = this.getByteLength(queryString);
|
|
39
39
|
const isEligibleForGetRequest = byteLength <= 9e3;
|
|
40
|
-
const
|
|
41
|
-
|
|
40
|
+
const minimalClientConfig = typeof this.config.minimalClient === "object" ? this.config.minimalClient : {};
|
|
41
|
+
const useCache = minimalClientConfig.cachingEnabled && isEligibleForGetRequest;
|
|
42
|
+
const requestUrl = useCache && minimalClientConfig.cacheBaseUrl ? minimalClientConfig.cacheBaseUrl + minimalClientConfig.queryEndpoint : `https://${this.config.projectId}.${this.config.useCdn && isEligibleForGetRequest ? API_CDN_HOST : API_HOST}${this.queryPath}`;
|
|
43
|
+
return (await $fetch(requestUrl + queryString, {
|
|
42
44
|
...this.fetchOptions,
|
|
43
45
|
method: isEligibleForGetRequest ? "GET" : "POST",
|
|
44
46
|
body: !isEligibleForGetRequest ? { query, params } : void 0
|
|
@@ -52,7 +54,8 @@ class MinimalSanityClient extends SanityClient {
|
|
|
52
54
|
apiVersion: this.config.apiVersion,
|
|
53
55
|
withCredentials: this.config.withCredentials,
|
|
54
56
|
token: this.config.token,
|
|
55
|
-
perspective: this.config.perspective
|
|
57
|
+
perspective: this.config.perspective,
|
|
58
|
+
minimalClient: this.config.minimalClient
|
|
56
59
|
});
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<NuxtImg
|
|
3
3
|
v-if="imageAsset?._id"
|
|
4
|
-
provider="sanity"
|
|
5
4
|
densities="x1 x2"
|
|
6
5
|
:src="imageAsset._id"
|
|
7
6
|
:width="imageAsset.metadata.dimensions.width"
|
|
8
7
|
:height="imageAsset.metadata.dimensions.height"
|
|
9
|
-
:alt="imageAsset.altText"
|
|
8
|
+
:alt="imageAsset.altText as (string | undefined)"
|
|
10
9
|
:placeholder="loading === 'eager' ? undefined : imageAsset.metadata.lqip"
|
|
11
10
|
:loading="loading || 'lazy'"
|
|
12
11
|
:format="imageAsset.mimeType === 'image/svg+xml' ? undefined : 'webp'"
|
|
13
12
|
draggable="false"
|
|
13
|
+
provider="cachedSanity"
|
|
14
14
|
/>
|
|
15
15
|
</template>
|
|
16
16
|
|
|
@@ -41,5 +41,4 @@ async function resolveImageAsset() {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
await resolveImageAsset()
|
|
44
|
-
watch(() => props.asset, resolveImageAsset)
|
|
45
44
|
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { joinURL } from "ufo";
|
|
2
|
+
import { useSanityVisualEditingState } from "../composables/useSanityVisualEditingState.js";
|
|
3
|
+
import { useRuntimeConfig } from "#imports";
|
|
4
|
+
import { getImage as getSanityImage } from "#image/providers/sanity";
|
|
5
|
+
export const getImage = (src, { modifiers = {} } = {}, ctx) => {
|
|
6
|
+
const sanityConfig = useRuntimeConfig().public.sanity;
|
|
7
|
+
const minimalClientConfig = sanityConfig.minimalClient;
|
|
8
|
+
const useCaching = typeof minimalClientConfig === "object" && minimalClientConfig.cachingEnabled;
|
|
9
|
+
if (useCaching && minimalClientConfig.cacheBaseUrl && !useSanityVisualEditingState().enabled) {
|
|
10
|
+
const params = new URLSearchParams();
|
|
11
|
+
params.set("src", src);
|
|
12
|
+
params.set("modifiers", JSON.stringify(modifiers));
|
|
13
|
+
return { url: joinURL(minimalClientConfig.cacheBaseUrl, minimalClientConfig.assetEndpoint + `?${params.toString()}`) };
|
|
14
|
+
}
|
|
15
|
+
return getSanityImage(src, { modifiers, projectId: sanityConfig.projectId, dataset: sanityConfig.dataset }, ctx);
|
|
16
|
+
};
|
|
@@ -11,11 +11,12 @@ export default defineNuxtPlugin(() => {
|
|
|
11
11
|
const $config = useRuntimeConfig();
|
|
12
12
|
const { visualEditing } = $config.public.sanity;
|
|
13
13
|
if (import.meta.server) {
|
|
14
|
-
|
|
14
|
+
const previewModeId = $config.sanity.visualEditing.previewModeId;
|
|
15
|
+
if (visualEditing?.previewMode && previewModeId) {
|
|
15
16
|
const previewModeCookie = useCookie("__sanity_preview");
|
|
16
|
-
visualEditingState.enabled =
|
|
17
|
+
visualEditingState.enabled = previewModeId === previewModeCookie.value;
|
|
17
18
|
}
|
|
18
|
-
} else if (visualEditingState.enabled && visualEditing
|
|
19
|
+
} else if (visualEditingState.enabled && visualEditing?.mode !== "custom") {
|
|
19
20
|
switch (visualEditing?.mode) {
|
|
20
21
|
case "live-visual-editing":
|
|
21
22
|
case "visual-editing":
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createError, defineEventHandler, getRequestURL } from "h3";
|
|
2
|
+
import { hash } from "ohash";
|
|
3
|
+
import resolveSanityImageUrl from "../../utils/resolveSanityImageUrl.js";
|
|
4
|
+
import { useStorage } from "#imports";
|
|
5
|
+
const TTL = 60 * 60 * 24 * 365;
|
|
6
|
+
export default defineEventHandler(async (event) => {
|
|
7
|
+
const queryParams = getRequestURL(event).searchParams;
|
|
8
|
+
const src = queryParams.get("src");
|
|
9
|
+
if (!src) {
|
|
10
|
+
throw createError({ statusCode: 400, statusMessage: "Missing src parameter" });
|
|
11
|
+
}
|
|
12
|
+
const modifiers = JSON.parse(queryParams.get("modifiers") || "{}");
|
|
13
|
+
const hashedPath = hash(src + JSON.stringify(modifiers));
|
|
14
|
+
const dataCache = useStorage("sanityData");
|
|
15
|
+
const cachedAsset = await dataCache.getItemRaw(hashedPath);
|
|
16
|
+
if (cachedAsset) {
|
|
17
|
+
const meta = await dataCache.getMeta(hashedPath);
|
|
18
|
+
if (import.meta.dev) {
|
|
19
|
+
console.debug(`Cache hit for asset ${hashedPath}`);
|
|
20
|
+
}
|
|
21
|
+
return new Response(cachedAsset, {
|
|
22
|
+
headers: {
|
|
23
|
+
"Content-Type": meta.contentType || "application/octet-stream",
|
|
24
|
+
"Cache-Control": `public, max-age=${TTL}, immutable`
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const imageUrl = resolveSanityImageUrl(src, modifiers);
|
|
29
|
+
return await new Promise((resolve, reject) => $fetch(imageUrl, {
|
|
30
|
+
headers: {
|
|
31
|
+
"Accept-Encoding": "gzip, deflate"
|
|
32
|
+
},
|
|
33
|
+
method: "GET",
|
|
34
|
+
responseType: "stream",
|
|
35
|
+
onResponseError: ({ response }) => reject(response),
|
|
36
|
+
async onResponse({ response }) {
|
|
37
|
+
if (response.status === 200) {
|
|
38
|
+
const assetBinaryData = Buffer.from(await response.arrayBuffer());
|
|
39
|
+
await dataCache.setItemRaw(hashedPath, assetBinaryData, { ttl: TTL });
|
|
40
|
+
await dataCache.setMeta(hashedPath, { contentType: response.headers.get("Content-Type") }, { ttl: TTL });
|
|
41
|
+
const assetId = src.split("/").pop();
|
|
42
|
+
if (assetId) {
|
|
43
|
+
const sanityDocumentDeps = useStorage("sanityDocumentDeps");
|
|
44
|
+
const documentDeps = await sanityDocumentDeps.getItem(assetId) || [];
|
|
45
|
+
documentDeps.push(hashedPath);
|
|
46
|
+
await sanityDocumentDeps.setItem(assetId, documentDeps);
|
|
47
|
+
if (import.meta.dev) {
|
|
48
|
+
console.debug(`Cache miss for asset ${hashedPath} (${assetId})`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
resolve(new Response(assetBinaryData, {
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": response.headers.get("Content-Type") || "application/octet-stream",
|
|
54
|
+
"Cache-Control": `public, max-age=${TTL}, immutable`
|
|
55
|
+
}
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createError,
|
|
3
|
+
defineEventHandler,
|
|
4
|
+
getRequestURL,
|
|
5
|
+
setResponseHeader
|
|
6
|
+
} from "h3";
|
|
7
|
+
import { hash } from "ohash";
|
|
8
|
+
import useSanityClient from "../../utils/useSanityClient.js";
|
|
9
|
+
import { useRuntimeConfig, useStorage } from "#imports";
|
|
10
|
+
const TTL = 60 * 60 * 8;
|
|
11
|
+
export default defineEventHandler(async (event) => {
|
|
12
|
+
const url = getRequestURL(event);
|
|
13
|
+
const queryParams = url.searchParams;
|
|
14
|
+
const query = queryParams.get("query");
|
|
15
|
+
if (!query)
|
|
16
|
+
throw createError({ statusCode: 400, statusMessage: "Missing query parameter" });
|
|
17
|
+
const params = Array.from(queryParams).filter(([key]) => key.startsWith("$"));
|
|
18
|
+
const perspective = queryParams.get("perspective") || useRuntimeConfig().public.sanity.perspective;
|
|
19
|
+
if (typeof perspective === "string" && !["previewDrafts", "published", "raw"].includes(perspective))
|
|
20
|
+
throw createError({ statusCode: 400, statusMessage: "Invalid perspective" });
|
|
21
|
+
const hashedQuery = hash(query + JSON.stringify(params) + (perspective || ""));
|
|
22
|
+
const dataCache = useStorage("sanityData");
|
|
23
|
+
const cachedResult = await dataCache.getItem(hashedQuery);
|
|
24
|
+
if (cachedResult) {
|
|
25
|
+
setResponseHeader(event, "X-Cache", "hit");
|
|
26
|
+
setResponseHeader(event, "Content-Type", "application/json");
|
|
27
|
+
if (import.meta.dev) {
|
|
28
|
+
console.debug(`Cache hit for query ${hashedQuery}`);
|
|
29
|
+
}
|
|
30
|
+
return { result: cachedResult };
|
|
31
|
+
}
|
|
32
|
+
const client = useSanityClient("minimal");
|
|
33
|
+
delete client.config.minimalClient;
|
|
34
|
+
client.config.useCdn = false;
|
|
35
|
+
const result = await client.fetch(
|
|
36
|
+
query,
|
|
37
|
+
Object.fromEntries(params.map(([key, value]) => [key.slice(1), JSON.parse(value)])),
|
|
38
|
+
{ perspective }
|
|
39
|
+
);
|
|
40
|
+
if (!result)
|
|
41
|
+
throw createError({ statusCode: 400, statusMessage: "Invalid query" });
|
|
42
|
+
await dataCache.setItem(hashedQuery, result, { ttl: TTL });
|
|
43
|
+
const stringifiedResult = JSON.stringify(result);
|
|
44
|
+
const referencedIds = stringifiedResult.match(/"(_ref|_id)":\s*"(.*?)"/g) || [];
|
|
45
|
+
if (referencedIds.length > 0) {
|
|
46
|
+
const sanityDocumentDeps = useStorage("sanityDocumentDeps");
|
|
47
|
+
await Promise.all(referencedIds.map((ref) => new Promise((resolve) => {
|
|
48
|
+
const id = ref.split('"')[3];
|
|
49
|
+
sanityDocumentDeps.getItem(id).then((deps) => {
|
|
50
|
+
const documentDeps = deps || [];
|
|
51
|
+
documentDeps.push(hashedQuery);
|
|
52
|
+
sanityDocumentDeps.setItem(id, documentDeps).then(resolve);
|
|
53
|
+
});
|
|
54
|
+
})));
|
|
55
|
+
}
|
|
56
|
+
setResponseHeader(event, "Content-Type", "application/json");
|
|
57
|
+
if (import.meta.dev) {
|
|
58
|
+
console.debug(`Cache miss for query ${hashedQuery} (${referencedIds.join(", ")})`);
|
|
59
|
+
}
|
|
60
|
+
return { result };
|
|
61
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { defineEventHandler, readRawBody, setResponseStatus, getRequestHeader } from "h3";
|
|
3
|
+
import { useRuntimeConfig, useStorage } from "#imports";
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const body = await readRawBody(event);
|
|
6
|
+
const signatureHeader = getRequestHeader(event, "sanity-webhook-signature");
|
|
7
|
+
const signatureParts = signatureHeader?.split(",");
|
|
8
|
+
if (!signatureParts || signatureParts.length !== 2) {
|
|
9
|
+
return setResponseStatus(event, 400, "Invalid signature");
|
|
10
|
+
}
|
|
11
|
+
const timestamp = Number.parseInt(signatureParts[0].slice(2), 10);
|
|
12
|
+
const signature = signatureParts[1].slice(3);
|
|
13
|
+
const secret = useRuntimeConfig().sanity.webhookSecret;
|
|
14
|
+
const expectedSignature = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("base64");
|
|
15
|
+
const expectedSignatureUrl = expectedSignature.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
16
|
+
if (signature !== expectedSignatureUrl) {
|
|
17
|
+
return setResponseStatus(event, 403);
|
|
18
|
+
}
|
|
19
|
+
const { _id } = JSON.parse(body || "{}");
|
|
20
|
+
const sanityDocumentDeps = useStorage("sanityDocumentDeps");
|
|
21
|
+
const documentDeps = await sanityDocumentDeps.getItem(_id);
|
|
22
|
+
if (documentDeps && documentDeps?.length > 0) {
|
|
23
|
+
const dataCache = useStorage("sanityData");
|
|
24
|
+
await Promise.all(documentDeps.map((key) => dataCache.removeItem(key)));
|
|
25
|
+
await dataCache.dispose();
|
|
26
|
+
}
|
|
27
|
+
await sanityDocumentDeps.removeItem(_id);
|
|
28
|
+
await sanityDocumentDeps.dispose();
|
|
29
|
+
if (import.meta.dev) {
|
|
30
|
+
console.debug(`Cleared cache for document ${_id} (${documentDeps?.length || 0} entries removed)`);
|
|
31
|
+
}
|
|
32
|
+
setResponseStatus(event, 204);
|
|
33
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { joinURL } from "ufo";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
const transformationMap = {
|
|
4
|
+
"format": "fm",
|
|
5
|
+
"height": "h",
|
|
6
|
+
"quality": "q",
|
|
7
|
+
"width": "w",
|
|
8
|
+
"background": "bg",
|
|
9
|
+
"download": "dl",
|
|
10
|
+
"dpr": "dpr",
|
|
11
|
+
"sharpen": "sharp",
|
|
12
|
+
"orientation": "or",
|
|
13
|
+
"min-height": "min-h",
|
|
14
|
+
"max-height": "max-h",
|
|
15
|
+
"min-width": "min-w",
|
|
16
|
+
"max-width": "max-w",
|
|
17
|
+
"minHeight": "min-h",
|
|
18
|
+
"maxHeight": "max-h",
|
|
19
|
+
"minWidth": "min-w",
|
|
20
|
+
"maxWidth": "max-w",
|
|
21
|
+
"saturation": "sat"
|
|
22
|
+
};
|
|
23
|
+
export default function resolveSanityImageUrl(src, modifiers) {
|
|
24
|
+
const matchResult = src.match(/-(?<width>\d*)x(?<height>\d*)-(?<format>.*)$/);
|
|
25
|
+
if (!matchResult || !matchResult.groups) {
|
|
26
|
+
throw new Error("Invalid image URL");
|
|
27
|
+
}
|
|
28
|
+
const sourceWidth = Number.parseInt(matchResult.groups.width);
|
|
29
|
+
const sourceHeight = Number.parseInt(matchResult.groups.height);
|
|
30
|
+
if (modifiers.crop && typeof modifiers.crop !== "string" && sourceWidth && sourceHeight) {
|
|
31
|
+
const left = modifiers.crop.left * sourceWidth;
|
|
32
|
+
const top = modifiers.crop.top * sourceHeight;
|
|
33
|
+
const right = sourceWidth - modifiers.crop.right * sourceWidth;
|
|
34
|
+
const bottom = sourceHeight - modifiers.crop.bottom * sourceHeight;
|
|
35
|
+
modifiers.rect = [left, top, right - left, bottom - top].map((i) => i.toFixed(0)).join(",");
|
|
36
|
+
delete modifiers.crop;
|
|
37
|
+
}
|
|
38
|
+
if (modifiers.hotspot && typeof modifiers.hotspot !== "string") {
|
|
39
|
+
modifiers["fp-x"] = modifiers.hotspot.x;
|
|
40
|
+
modifiers["fp-y"] = modifiers.hotspot.y;
|
|
41
|
+
delete modifiers.hotspot;
|
|
42
|
+
}
|
|
43
|
+
if (!modifiers.format || modifiers.format === "auto") {
|
|
44
|
+
modifiers.auto = "format";
|
|
45
|
+
}
|
|
46
|
+
if (modifiers.fit === "contain" && !modifiers.bg)
|
|
47
|
+
modifiers.bg = "ffffff";
|
|
48
|
+
const operations = Object.keys(modifiers).map((key) => {
|
|
49
|
+
const operationKey = transformationMap[key] || key;
|
|
50
|
+
let value = modifiers[key];
|
|
51
|
+
if (operationKey === "fm" && value === "jpeg")
|
|
52
|
+
value = "jpg";
|
|
53
|
+
if (operationKey === "fit") {
|
|
54
|
+
if (value === "cover")
|
|
55
|
+
value = "crop";
|
|
56
|
+
else if (value === "contain")
|
|
57
|
+
value = "fill";
|
|
58
|
+
else if (value === "fill")
|
|
59
|
+
value = "scale";
|
|
60
|
+
else if (value === "inside")
|
|
61
|
+
value = "min";
|
|
62
|
+
else if (value === "outside")
|
|
63
|
+
value = "max";
|
|
64
|
+
}
|
|
65
|
+
if (value === true)
|
|
66
|
+
return operationKey;
|
|
67
|
+
return `${operationKey}=${value}`;
|
|
68
|
+
}).join("&");
|
|
69
|
+
const parts = src.split("-").slice(1);
|
|
70
|
+
const format = parts.pop();
|
|
71
|
+
const filenameAndQueries = parts.join("-") + "." + format + (operations ? "?" + operations : "");
|
|
72
|
+
const sanityConfig = useRuntimeConfig().public.sanity;
|
|
73
|
+
return joinURL("https://cdn.sanity.io/images", sanityConfig.projectId, sanityConfig.dataset, filenameAndQueries);
|
|
74
|
+
}
|
|
@@ -4,7 +4,7 @@ import { useRuntimeConfig } from "#imports";
|
|
|
4
4
|
export default function useSanityClient(type) {
|
|
5
5
|
const $config = useRuntimeConfig();
|
|
6
6
|
const sanityConfig = defu($config.sanity, $config.public.sanity || {});
|
|
7
|
-
const visualEditingEnabled = sanityConfig.visualEditing && sanityConfig.visualEditing.previewMode !== false;
|
|
7
|
+
const visualEditingEnabled = type !== "minimal" && sanityConfig.visualEditing && sanityConfig.visualEditing.previewMode !== false;
|
|
8
8
|
return getOrCreateSanityClient(
|
|
9
9
|
visualEditingEnabled || false,
|
|
10
10
|
visualEditingEnabled ? {
|
package/dist/types.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { type GlobalSEO, type Home, type LinkExternal, type LinkInternal, type ModuleOptions, type NotFound, type Page, type RichText, type SEO, type SanityArray, type SanityModule, type SanityVisualEditingRefreshHandler, type VisualEditingOptions, default } from './module.js'
|
|
1
|
+
export { type GlobalSEO, type Home, type LinkExternal, type LinkInternal, type ModuleOptions, type NotFound, type Page, type RichText, type SEO, type SanityArray, type SanityModule, type SanityVisualEditingMode, type SanityVisualEditingRefreshHandler, type VisualEditingOptions, default } from './module.js'
|
package/dist/types.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { type GlobalSEO, type Home, type LinkExternal, type LinkInternal, type ModuleOptions, type NotFound, type Page, type RichText, type SEO, type SanityArray, type SanityModule, type SanityVisualEditingRefreshHandler, type VisualEditingOptions, default } from './module'
|
|
1
|
+
export { type GlobalSEO, type Home, type LinkExternal, type LinkInternal, type ModuleOptions, type NotFound, type Page, type RichText, type SEO, type SanityArray, type SanityModule, type SanityVisualEditingMode, type SanityVisualEditingRefreshHandler, type VisualEditingOptions, default } from './module'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devite/nuxt-sanity",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Advanced Sanity integration for Nuxt.js.",
|
|
5
5
|
"repository": "devite-io/nuxt-sanity",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"@sanity/visual-editing": "^2.11.0",
|
|
33
33
|
"defu": "^6.1.4",
|
|
34
34
|
"ofetch": "^1.4.1",
|
|
35
|
-
"ohash": "^1.1.4"
|
|
35
|
+
"ohash": "^1.1.4",
|
|
36
|
+
"unstorage": "^1.14.1"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@nuxt/eslint-config": "^0.7.4",
|