@towa-digital/storyblok-nuxt-cv 0.3.0 → 0.5.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/README.md +25 -6
- package/dist/module.d.mts +10 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +5 -0
- package/dist/runtime/server/middleware/cv.d.ts +0 -9
- package/dist/runtime/server/middleware/cv.js +10 -1
- package/dist/runtime/server/plugins/mount.d.ts +8 -0
- package/dist/runtime/server/plugins/mount.js +8 -0
- package/dist/runtime/server/utils/cv.d.ts +5 -4
- package/dist/runtime/server/utils/cv.js +38 -4
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ export default defineNuxtConfig({
|
|
|
72
72
|
region: 'eu', // eu | us | ap | ca | cn
|
|
73
73
|
endpoint: undefined, // full API endpoint incl. /v2 (like @storyblok/nuxt); overrides region — for proxied spaces
|
|
74
74
|
storage: 'storyblok-cv', // nitro storage mount name
|
|
75
|
+
fsStore: false, // true → module mounts an fs store at <cwd>/.data/<storage> at RUNTIME (node/PM2; owner-correct, per-deploy). string = custom base.
|
|
75
76
|
statusRoute: '/api/storyblok-cv', // GET current cv; false to disable
|
|
76
77
|
webhook: true, // built-in route /api/storyblok-cv-webhook; string = custom route; false = use refreshStoryblokCv() yourself
|
|
77
78
|
ttl: false, // seconds, e.g. 60 — background refresh when the stored cv is older (serverless/multi-instance)
|
|
@@ -85,18 +86,36 @@ export default defineNuxtConfig({
|
|
|
85
86
|
| Target | Storage | Strategy |
|
|
86
87
|
|---|---|---|
|
|
87
88
|
| Single Node process (DO app platform, plain node-server) | default (memory) | webhook (default setup) |
|
|
88
|
-
| PM2 **cluster** on one host (`instances: 'max'`) |
|
|
89
|
+
| PM2 **cluster** on one host (`instances: 'max'`) | **`fsStore: true`** (or a manual `fs` mount) | webhook |
|
|
89
90
|
| Multi-node / containers | mount Redis | webhook |
|
|
90
91
|
| Cloudflare / Vercel Edge / Netlify (isolates) | default | **`ttl` (seconds, e.g. `60`)** — a webhook can't reach every isolate |
|
|
91
92
|
|
|
93
|
+
On a **PM2 cluster** memory is per-worker, so a webhook only updates one worker.
|
|
94
|
+
The simplest correct fix is **`fsStore`** — the module mounts an `fs` store at a
|
|
95
|
+
path resolved **at runtime**, inside the app's own directory (owned by the
|
|
96
|
+
runtime user, unique per deployment):
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
storyblokCv: { fsStore: true } // → <cwd>/.data/storyblok-cv (or fsStore: '/abs/path')
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Prefer this over hand-configuring `nitro.storage` — it's owner-correct and
|
|
103
|
+
per-deploy, so it sidesteps the gotchas below. **Node/PM2 only** — serverless/edge
|
|
104
|
+
have no writable fs, use `ttl` there.
|
|
105
|
+
|
|
106
|
+
<details><summary>If you configure the <code>fs</code> mount yourself instead</summary>
|
|
107
|
+
|
|
92
108
|
```ts
|
|
93
|
-
|
|
94
|
-
// only update the one worker that received it:
|
|
95
|
-
nitro: {
|
|
96
|
-
storage: { 'storyblok-cv': { driver: 'fs', base: './.data/storyblok-cv' } },
|
|
97
|
-
},
|
|
109
|
+
nitro: { storage: { 'storyblok-cv': { driver: 'fs', base: '/absolute/writable/path' } } }
|
|
98
110
|
```
|
|
99
111
|
|
|
112
|
+
Use an **absolute** path writable by the runtime user. A *relative* `base` is
|
|
113
|
+
resolved at **build time** (it bakes the build/CI cwd, not the server's) — which
|
|
114
|
+
is exactly what `fsStore` sidesteps by resolving at runtime. Persist failures are
|
|
115
|
+
logged as `[storyblok-cv] … FAILED to persist`.
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
100
119
|
If unsure, setting `ttl` **in addition to** the webhook is a safe belt-and-braces
|
|
101
120
|
default: staleness is then bounded by the TTL even if webhook delivery breaks.
|
|
102
121
|
|
package/dist/module.d.mts
CHANGED
|
@@ -27,6 +27,15 @@ interface ModuleOptions {
|
|
|
27
27
|
* an `fs` driver, for multi-node mount Redis, in the host's `nitro.storage`.
|
|
28
28
|
*/
|
|
29
29
|
storage?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Mount the `cv` store on the `fs` driver at a path resolved **at runtime** —
|
|
32
|
+
* in the app's own dir, owned by the runtime user, unique per deploy.
|
|
33
|
+
* `true` → `<cwd>/.data/<storage>`; a string sets the base (relative resolved
|
|
34
|
+
* against `process.cwd()`). Node/PM2 only — not serverless/edge (no writable
|
|
35
|
+
* fs; use `ttl`). Runtime resolution avoids a static `nitro.storage` base
|
|
36
|
+
* baking the build-time cwd.
|
|
37
|
+
*/
|
|
38
|
+
fsStore?: boolean | string;
|
|
30
39
|
/** GET route exposing the current `cv` (client re-pins, debugging). `false` disables. */
|
|
31
40
|
statusRoute?: string | false;
|
|
32
41
|
/**
|
|
@@ -55,6 +64,7 @@ declare module '@nuxt/schema' {
|
|
|
55
64
|
token: string;
|
|
56
65
|
endpoint: string;
|
|
57
66
|
storage: string;
|
|
67
|
+
fsStore: boolean | string;
|
|
58
68
|
ttl: number;
|
|
59
69
|
onStartup: boolean;
|
|
60
70
|
webhookSecret: string;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -19,6 +19,7 @@ const module$1 = defineNuxtModule({
|
|
|
19
19
|
debug: false,
|
|
20
20
|
region: "eu",
|
|
21
21
|
storage: "storyblok-cv",
|
|
22
|
+
fsStore: false,
|
|
22
23
|
statusRoute: "/api/storyblok-cv",
|
|
23
24
|
webhook: true,
|
|
24
25
|
ttl: false,
|
|
@@ -44,6 +45,7 @@ const module$1 = defineNuxtModule({
|
|
|
44
45
|
token: token ?? "",
|
|
45
46
|
endpoint: options.endpoint || API_ENDPOINTS[options.region] || API_ENDPOINTS.eu,
|
|
46
47
|
storage: options.storage,
|
|
48
|
+
fsStore: options.fsStore ?? false,
|
|
47
49
|
// Option is in seconds (nicer DX); convert to ms for the runtime, which
|
|
48
50
|
// compares against `Date.now()`.
|
|
49
51
|
ttl: options.ttl === false ? 0 : (options.ttl ?? 0) * 1e3,
|
|
@@ -69,6 +71,9 @@ const module$1 = defineNuxtModule({
|
|
|
69
71
|
handler: resolver.resolve("./runtime/server/api/webhook.post")
|
|
70
72
|
});
|
|
71
73
|
}
|
|
74
|
+
if (options.fsStore) {
|
|
75
|
+
addServerPlugin(resolver.resolve("./runtime/server/plugins/mount"));
|
|
76
|
+
}
|
|
72
77
|
addServerPlugin(resolver.resolve("./runtime/server/plugins/init"));
|
|
73
78
|
const utils = resolver.resolve("./runtime/server/utils/cv");
|
|
74
79
|
addServerImports([
|
|
@@ -1,11 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Attach the stored `cv` to `event.context.storyblokCv` for every page render;
|
|
3
|
-
* the app plugin reads it from there (no HTTP self-fetch) and pins it on the
|
|
4
|
-
* render client before content is fetched.
|
|
5
|
-
*
|
|
6
|
-
* With `ttl` configured, an expired value also triggers a background refresh
|
|
7
|
-
* (stale-while-revalidate) — the freshness mechanism for serverless/edge
|
|
8
|
-
* targets and multi-instance setups a webhook cannot reliably reach.
|
|
9
|
-
*/
|
|
10
1
|
declare const _default: any;
|
|
11
2
|
export default _default;
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { defineEventHandler, useRuntimeConfig } from "#imports";
|
|
2
2
|
import { readStoryblokCv, refreshStoryblokCv } from "../utils/cv.js";
|
|
3
|
+
let lastMapSyncCheck = 0;
|
|
4
|
+
const MAP_SYNC_INTERVAL = 5e3;
|
|
3
5
|
export default defineEventHandler(async (event) => {
|
|
4
6
|
const path = event.path;
|
|
5
|
-
if (path.startsWith("/api/") || path.startsWith("/_") || path.startsWith("/__"))
|
|
7
|
+
if (path.startsWith("/api/") || path.startsWith("/_") || path.startsWith("/__")) {
|
|
8
|
+
if (Date.now() - lastMapSyncCheck > MAP_SYNC_INTERVAL) {
|
|
9
|
+
lastMapSyncCheck = Date.now();
|
|
10
|
+
readStoryblokCv().catch(() => {
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
6
15
|
const { ttl } = useRuntimeConfig().storyblokCv;
|
|
7
16
|
const record = await readStoryblokCv();
|
|
8
17
|
if (record?.cv) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mount the `cv` store on the fs driver at a path resolved at runtime — in the
|
|
3
|
+
* app's own dir, owned by the runtime user, unique per deploy. Runs before the
|
|
4
|
+
* init plugin (mounts before the startup refresh writes); only registered when
|
|
5
|
+
* `fsStore` is set, so edge builds never import the fs driver.
|
|
6
|
+
*/
|
|
7
|
+
declare const _default: any;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { isAbsolute, join } from "node:path";
|
|
2
|
+
import fsDriver from "unstorage/drivers/fs-lite";
|
|
3
|
+
import { defineNitroPlugin, useRuntimeConfig, useStorage } from "#imports";
|
|
4
|
+
export default defineNitroPlugin(() => {
|
|
5
|
+
const { storage, fsStore } = useRuntimeConfig().storyblokCv;
|
|
6
|
+
const base = typeof fsStore === "string" ? isAbsolute(fsStore) ? fsStore : join(process.cwd(), fsStore) : join(process.cwd(), ".data", storage);
|
|
7
|
+
useStorage().mount(storage, fsDriver({ base }));
|
|
8
|
+
});
|
|
@@ -8,12 +8,13 @@ export interface StoryblokCvRecord {
|
|
|
8
8
|
* touch any client copy at all, so it works with client v5 through v7 alike.
|
|
9
9
|
*/
|
|
10
10
|
export declare function getLiveStoryblokCv(): Promise<number>;
|
|
11
|
-
/** Stored `cv` record from the
|
|
11
|
+
/** Stored `cv` record from the storage mount, or `null` before the first refresh. */
|
|
12
12
|
export declare function readStoryblokCv(): Promise<StoryblokCvRecord | null>;
|
|
13
|
+
export declare function syncServerClientCv(cv: number): void;
|
|
13
14
|
/**
|
|
14
|
-
* Fetch the live `cv` and persist it to
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Fetch the live `cv` and persist it to storage. Auto-imported in server code —
|
|
16
|
+
* call this from your own publish-webhook handler when the built-in webhook
|
|
17
|
+
* route is disabled. Concurrent calls are deduped.
|
|
17
18
|
*
|
|
18
19
|
* @returns the live `cv` (`0` if the API reported none)
|
|
19
20
|
*/
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { $fetch } from "ofetch";
|
|
2
2
|
import { useRuntimeConfig, useStorage } from "#imports";
|
|
3
3
|
const KEY = "current";
|
|
4
|
-
const
|
|
4
|
+
const cfg = () => useRuntimeConfig().storyblokCv;
|
|
5
|
+
const store = () => useStorage(cfg().storage);
|
|
5
6
|
export async function getLiveStoryblokCv() {
|
|
6
|
-
const { token, endpoint } =
|
|
7
|
+
const { token, endpoint } = cfg();
|
|
7
8
|
if (!token) {
|
|
8
9
|
throw new Error("[storyblok-cv] no access token configured");
|
|
9
10
|
}
|
|
@@ -14,13 +15,46 @@ export async function getLiveStoryblokCv() {
|
|
|
14
15
|
return response?.space?.version ?? 0;
|
|
15
16
|
}
|
|
16
17
|
export async function readStoryblokCv() {
|
|
17
|
-
|
|
18
|
+
const record = await store().getItem(KEY);
|
|
19
|
+
if (record?.cv) syncServerClientCv(record.cv);
|
|
20
|
+
return record;
|
|
21
|
+
}
|
|
22
|
+
let serverClient = null;
|
|
23
|
+
let lastSyncedCv = 0;
|
|
24
|
+
export function syncServerClientCv(cv) {
|
|
25
|
+
if (!cv || cv === lastSyncedCv || serverClient === false) return;
|
|
26
|
+
if (serverClient) {
|
|
27
|
+
lastSyncedCv = cv;
|
|
28
|
+
serverClient.setCacheVersion(cv);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
import("storyblok-js-client").then((mod) => {
|
|
32
|
+
const StoryblokClient = mod?.default;
|
|
33
|
+
const { token } = cfg();
|
|
34
|
+
if (!StoryblokClient || !token) {
|
|
35
|
+
serverClient = false;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
serverClient = new StoryblokClient({ accessToken: token });
|
|
39
|
+
lastSyncedCv = cv;
|
|
40
|
+
serverClient.setCacheVersion(cv);
|
|
41
|
+
}).catch(() => {
|
|
42
|
+
serverClient = false;
|
|
43
|
+
});
|
|
18
44
|
}
|
|
19
45
|
let pending = null;
|
|
20
46
|
export function refreshStoryblokCv() {
|
|
21
47
|
pending ??= getLiveStoryblokCv().then(async (cv) => {
|
|
22
48
|
if (cv) {
|
|
23
|
-
|
|
49
|
+
syncServerClientCv(cv);
|
|
50
|
+
try {
|
|
51
|
+
await store().setItem(KEY, { cv, fetchedAt: Date.now() });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(
|
|
54
|
+
"[storyblok-cv] fetched cv but FAILED to persist it \u2014 check the storage mount is writable by the runtime user:",
|
|
55
|
+
error
|
|
56
|
+
);
|
|
57
|
+
}
|
|
24
58
|
}
|
|
25
59
|
return cv;
|
|
26
60
|
}).finally(() => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towa-digital/storyblok-nuxt-cv",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Keeps Storyblok published content fresh on long-running Nuxt servers by bridging the live cache version (cv) to the render client
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Keeps Storyblok published content fresh on long-running Nuxt servers by bridging the live cache version (cv) to the render client \u2014 no redeploy after publish.",
|
|
5
5
|
"repository": "github:towa-digital/storyblok-nuxt-cv",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Matthias Frank",
|
|
@@ -55,5 +55,6 @@
|
|
|
55
55
|
"typescript": "^5.8.0",
|
|
56
56
|
"vitest": "^3.2.0",
|
|
57
57
|
"vue-tsc": "^2.2.10"
|
|
58
|
-
}
|
|
58
|
+
},
|
|
59
|
+
"packageManager": "yarn@4.11.0+sha256.b0efc6597348fab50c777d2ff4b09e814974058cf012c3d687c9e80bca824358"
|
|
59
60
|
}
|