@xosen/site-sdk 0.0.9 → 0.0.10
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/composables/usePageData.d.ts +26 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +178 -121
- package/package.json +1 -1
- package/src/composables/usePageData.ts +153 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Ref } from 'vue';
|
|
2
|
+
import type { Block } from '../types/blocks.js';
|
|
3
|
+
import type { PageConfig, PageContext } from '../types/config.js';
|
|
4
|
+
export interface UsePageDataReturn {
|
|
5
|
+
/** Resolved page data (from preload, API, or preview) */
|
|
6
|
+
page: Ref<PageConfig | null>;
|
|
7
|
+
/** Blocks to render (preview-aware) */
|
|
8
|
+
blocks: Ref<Block[]>;
|
|
9
|
+
/** Page context (metadata without blocks) */
|
|
10
|
+
pageContext: Ref<PageContext>;
|
|
11
|
+
/** True while fetching from API */
|
|
12
|
+
loading: Ref<boolean>;
|
|
13
|
+
/** True if page was not found */
|
|
14
|
+
notFound: Ref<boolean>;
|
|
15
|
+
/** True if in live preview mode */
|
|
16
|
+
isPreviewMode: Ref<boolean>;
|
|
17
|
+
/** Reload page data from API */
|
|
18
|
+
reload: () => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Composable for loading page data with preloaded data, API fallback,
|
|
22
|
+
* locale switching, live preview support, and meta tag injection.
|
|
23
|
+
*
|
|
24
|
+
* @param slug - Reactive slug ref or static string (e.g. 'home', 'pricing')
|
|
25
|
+
*/
|
|
26
|
+
export declare function usePageData(slug: Ref<string> | string): UsePageDataReturn;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export { useSiteConfig } from './composables/useSiteConfig.js';
|
|
|
3
3
|
export { useSkin } from './composables/useSkin.js';
|
|
4
4
|
export { useSiteApi } from './composables/useSiteApi.js';
|
|
5
5
|
export { useLivePreview, setupPreviewRouter, isInPreview } from './composables/useLivePreview.js';
|
|
6
|
+
export { usePageData } from './composables/usePageData.js';
|
|
7
|
+
export type { UsePageDataReturn } from './composables/usePageData.js';
|
|
6
8
|
export type { PreviewMessage } from './composables/useLivePreview.js';
|
|
7
9
|
export { createSiteWorker } from './worker/index.js';
|
|
8
10
|
export type { WorkerEnv, WorkerConfig, PageData } from './worker/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,131 +1,188 @@
|
|
|
1
|
-
import { ref as y, onMounted as
|
|
2
|
-
import { useI18n as
|
|
3
|
-
function
|
|
4
|
-
const t = y(null), { locale: e } =
|
|
5
|
-
function
|
|
6
|
-
return
|
|
1
|
+
import { ref as y, onMounted as S, watch as _, computed as f, onUnmounted as x, provide as C } from "vue";
|
|
2
|
+
import { useI18n as b } from "vue-i18n";
|
|
3
|
+
function k(o, n) {
|
|
4
|
+
const t = y(null), { locale: e } = b(), s = window.__SITE_DATA__, a = s?.locale;
|
|
5
|
+
function l() {
|
|
6
|
+
return s && s[o] && e.value === a ? (t.value = s[o], !0) : !1;
|
|
7
7
|
}
|
|
8
|
-
async function
|
|
8
|
+
async function u() {
|
|
9
9
|
try {
|
|
10
|
-
const
|
|
11
|
-
|
|
10
|
+
const i = await fetch(`/api/content/${n}:${e.value}`);
|
|
11
|
+
i.ok && (t.value = await i.json());
|
|
12
12
|
} catch {
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
}),
|
|
18
|
-
|
|
15
|
+
return S(() => {
|
|
16
|
+
l() || u();
|
|
17
|
+
}), _(e, () => {
|
|
18
|
+
u();
|
|
19
19
|
}), t;
|
|
20
20
|
}
|
|
21
|
-
function
|
|
22
|
-
const
|
|
23
|
-
function
|
|
24
|
-
const
|
|
25
|
-
return typeof
|
|
21
|
+
function M() {
|
|
22
|
+
const o = window.__SITE_DATA__, n = f(() => o?.config || null), t = f(() => o?.components || {}), e = f(() => n.value?.navigation || []), s = f(() => n.value?.locales || []), a = f(() => n.value?.defaultLocale || "en"), l = f(() => n.value?.branding || {}), u = f(() => n.value?.footer || {}), i = f(() => n.value?.features || {});
|
|
23
|
+
function g(w) {
|
|
24
|
+
const v = i.value[w];
|
|
25
|
+
return typeof v == "boolean" ? v : typeof v == "object";
|
|
26
26
|
}
|
|
27
27
|
return {
|
|
28
|
-
config:
|
|
28
|
+
config: n,
|
|
29
29
|
components: t,
|
|
30
30
|
navigation: e,
|
|
31
|
-
locales:
|
|
32
|
-
defaultLocale:
|
|
33
|
-
branding:
|
|
34
|
-
footer:
|
|
35
|
-
features:
|
|
36
|
-
hasFeature:
|
|
31
|
+
locales: s,
|
|
32
|
+
defaultLocale: a,
|
|
33
|
+
branding: l,
|
|
34
|
+
footer: u,
|
|
35
|
+
features: i,
|
|
36
|
+
hasFeature: g
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
function
|
|
40
|
-
function
|
|
39
|
+
function U(o) {
|
|
40
|
+
function n(t) {
|
|
41
41
|
const e = document.documentElement;
|
|
42
42
|
if (t.colors)
|
|
43
|
-
for (const [
|
|
44
|
-
|
|
43
|
+
for (const [s, a] of Object.entries(t.colors))
|
|
44
|
+
a && e.style.setProperty(`--x-color-${N(s)}`, a);
|
|
45
45
|
t.typography?.fontFamily && e.style.setProperty("--x-font-family", t.typography.fontFamily), t.typography?.headingFont && e.style.setProperty("--x-font-heading", t.typography.headingFont), t.typography?.baseFontSize && e.style.setProperty("--x-font-size", t.typography.baseFontSize), t.shape?.borderRadius && e.style.setProperty("--x-border-radius", t.shape.borderRadius), t.shape?.maxWidth && e.style.setProperty("--x-max-width", t.shape.maxWidth);
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
o
|
|
47
|
+
S(() => {
|
|
48
|
+
if (o) {
|
|
49
|
+
n(o);
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
const e = window.__SITE_DATA__?.skin;
|
|
53
|
-
e && (e.colors || e.typography || e.shape) &&
|
|
53
|
+
e && (e.colors || e.typography || e.shape) && n({
|
|
54
54
|
colors: e.colors || {},
|
|
55
55
|
typography: e.typography || {},
|
|
56
56
|
shape: e.shape || {}
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
|
-
function
|
|
61
|
-
return
|
|
60
|
+
function N(o) {
|
|
61
|
+
return o.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
62
62
|
}
|
|
63
|
-
function
|
|
64
|
-
const
|
|
65
|
-
async function t(
|
|
66
|
-
const
|
|
67
|
-
if (!
|
|
68
|
-
throw new Error(`API error: ${
|
|
69
|
-
return
|
|
63
|
+
function J() {
|
|
64
|
+
const n = window.__SITE_DATA__?.apiBase || "";
|
|
65
|
+
async function t(a) {
|
|
66
|
+
const l = await fetch(`${n}${a}`);
|
|
67
|
+
if (!l.ok)
|
|
68
|
+
throw new Error(`API error: ${l.status}`);
|
|
69
|
+
return l.json();
|
|
70
70
|
}
|
|
71
71
|
async function e() {
|
|
72
72
|
return t("/v1/billing/tariffs");
|
|
73
73
|
}
|
|
74
|
-
async function
|
|
74
|
+
async function s() {
|
|
75
75
|
return t("/v1/billing/services");
|
|
76
76
|
}
|
|
77
77
|
return {
|
|
78
78
|
get: t,
|
|
79
79
|
getTariffs: e,
|
|
80
|
-
getProducts:
|
|
80
|
+
getProducts: s
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
-
function
|
|
83
|
+
function E() {
|
|
84
84
|
try {
|
|
85
85
|
return window.self !== window.top;
|
|
86
86
|
} catch {
|
|
87
87
|
return !0;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
90
|
+
function z(o) {
|
|
91
|
+
E() && o.beforeEach((n, t) => !t.name);
|
|
92
92
|
}
|
|
93
|
-
function
|
|
94
|
-
const
|
|
93
|
+
function P() {
|
|
94
|
+
const o = y(null), n = y(E());
|
|
95
95
|
function t(e) {
|
|
96
|
-
const
|
|
97
|
-
|
|
96
|
+
const s = e.data;
|
|
97
|
+
s?.type === "xosen-preview-update" && s.page && (o.value = s.page, n.value = !0);
|
|
98
98
|
}
|
|
99
|
-
return
|
|
99
|
+
return S(() => {
|
|
100
100
|
window.addEventListener("message", t);
|
|
101
|
-
}),
|
|
101
|
+
}), x(() => {
|
|
102
102
|
window.removeEventListener("message", t);
|
|
103
103
|
}), {
|
|
104
|
-
previewPage:
|
|
105
|
-
isPreviewMode:
|
|
104
|
+
previewPage: o,
|
|
105
|
+
isPreviewMode: n
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
-
const
|
|
109
|
-
function
|
|
110
|
-
const e =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
108
|
+
const I = /* @__PURE__ */ Symbol("pageContext");
|
|
109
|
+
function K(o) {
|
|
110
|
+
const { locale: n } = b(), { previewPage: t, isPreviewMode: e } = P(), s = window.__SITE_DATA__, a = y(null), l = y(!0), u = y(!1), i = f(() => typeof o == "string" ? o : o.value), g = f(() => `page:${i.value}`), w = f(() => {
|
|
111
|
+
const r = e.value ? t.value : a.value;
|
|
112
|
+
if (!r) return {};
|
|
113
|
+
const { blocks: m, meta: c, slug: d, locale: j, ...A } = r;
|
|
114
|
+
return A;
|
|
115
|
+
});
|
|
116
|
+
C(I, w);
|
|
117
|
+
const v = f(() => e.value && t.value?.slug === i.value ? t.value.blocks || [] : a.value?.blocks || []);
|
|
118
|
+
function T() {
|
|
119
|
+
const r = s?.[g.value];
|
|
120
|
+
return r && n.value === s?.locale ? (a.value = r, u.value = !1, h(), !0) : !1;
|
|
121
|
+
}
|
|
122
|
+
async function p() {
|
|
123
|
+
l.value = !0, u.value = !1;
|
|
124
|
+
try {
|
|
125
|
+
const r = await fetch(`/api/content/${g.value}:${n.value}`);
|
|
126
|
+
r.ok ? (a.value = await r.json(), h()) : (a.value = null, u.value = !0);
|
|
127
|
+
} catch {
|
|
128
|
+
a.value = null, u.value = !0;
|
|
129
|
+
} finally {
|
|
130
|
+
l.value = !1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function h() {
|
|
134
|
+
if (!a.value) return;
|
|
135
|
+
if (a.value.title) {
|
|
136
|
+
const m = s?.config?.branding, c = m?.siteName ? ` — ${m.siteName}` : "";
|
|
137
|
+
document.title = `${a.value.title}${c}`;
|
|
138
|
+
}
|
|
139
|
+
const r = a.value.meta?.description;
|
|
140
|
+
if (r) {
|
|
141
|
+
const m = document.querySelector('meta[name="description"]');
|
|
142
|
+
m && m.setAttribute("content", r);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return _(t, (r) => {
|
|
146
|
+
r?.slug === i.value && (l.value = !1, u.value = !1, (r.title || r.blocks) && (a.value = {
|
|
147
|
+
...a.value || {},
|
|
148
|
+
...r.title ? { title: r.title } : {},
|
|
149
|
+
...r.blocks ? { blocks: r.blocks } : {}
|
|
150
|
+
}, h()));
|
|
151
|
+
}), S(() => {
|
|
152
|
+
T() ? l.value = !1 : p();
|
|
153
|
+
}), _(n, () => p()), _(i, () => {
|
|
154
|
+
T() ? l.value = !1 : p();
|
|
155
|
+
}), {
|
|
156
|
+
page: a,
|
|
157
|
+
blocks: v,
|
|
158
|
+
pageContext: w,
|
|
159
|
+
loading: l,
|
|
160
|
+
notFound: u,
|
|
161
|
+
isPreviewMode: e,
|
|
162
|
+
reload: p
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const L = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|webp|avif|map|json|txt|xml|webmanifest)$/;
|
|
166
|
+
function R(o, n) {
|
|
167
|
+
const e = new URL(o.url).searchParams.get("lang");
|
|
168
|
+
if (e && n.supportedLocales?.includes(e)) return e;
|
|
169
|
+
if (n.supportedLocales?.length) {
|
|
170
|
+
const s = o.headers.get("Accept-Language") || "";
|
|
171
|
+
for (const a of n.supportedLocales)
|
|
172
|
+
if (s.includes(a)) return a;
|
|
116
173
|
}
|
|
117
|
-
return
|
|
174
|
+
return n.defaultLocale;
|
|
118
175
|
}
|
|
119
|
-
function
|
|
120
|
-
if (!
|
|
121
|
-
const e = decodeURIComponent(
|
|
176
|
+
function O(o, n, t) {
|
|
177
|
+
if (!o.pathname.startsWith("/api/content/")) return null;
|
|
178
|
+
const e = decodeURIComponent(o.pathname.replace("/api/content/", ""));
|
|
122
179
|
return e ? (async () => {
|
|
123
|
-
let
|
|
124
|
-
if (!
|
|
125
|
-
const
|
|
126
|
-
|
|
180
|
+
let s = await n.SITE_CONTENT.get(e);
|
|
181
|
+
if (!s) {
|
|
182
|
+
const a = e.lastIndexOf(":");
|
|
183
|
+
a > 0 && e.slice(a + 1) !== t && (s = await n.SITE_CONTENT.get(e.slice(0, a + 1) + t));
|
|
127
184
|
}
|
|
128
|
-
return
|
|
185
|
+
return s ? new Response(s, {
|
|
129
186
|
headers: {
|
|
130
187
|
"Content-Type": "application/json",
|
|
131
188
|
"Cache-Control": "public, max-age=60",
|
|
@@ -134,54 +191,54 @@ function I(n, o, t) {
|
|
|
134
191
|
}) : Response.json({ error: "Not found" }, { status: 404 });
|
|
135
192
|
})() : Promise.resolve(Response.json({ error: "Key is required" }, { status: 400 }));
|
|
136
193
|
}
|
|
137
|
-
function
|
|
138
|
-
let e =
|
|
139
|
-
if (
|
|
140
|
-
const
|
|
141
|
-
e = e.replace(/<title>[^<]*<\/title>/, `<title>${
|
|
194
|
+
function $(o, n, t) {
|
|
195
|
+
let e = o;
|
|
196
|
+
if (n.title) {
|
|
197
|
+
const s = t ? ` — ${t}` : "";
|
|
198
|
+
e = e.replace(/<title>[^<]*<\/title>/, `<title>${n.title}${s}</title>`);
|
|
142
199
|
}
|
|
143
|
-
return
|
|
200
|
+
return n.meta?.description && (e = e.replace(/(<meta\s+name="description"\s+content=")[^"]*(")/, `$1${n.meta.description}$2`)), n.meta?.ogTitle && (e = e.replace(/(<meta\s+property="og:title"\s+content=")[^"]*(")/, `$1${n.meta.ogTitle}$2`)), n.html && (e = e.replace('<div id="app"></div>', `<div id="app">${n.html}</div>`)), e;
|
|
144
201
|
}
|
|
145
|
-
function
|
|
202
|
+
function W(o) {
|
|
146
203
|
return {
|
|
147
|
-
async fetch(
|
|
148
|
-
const e = new URL(
|
|
149
|
-
if (
|
|
150
|
-
if (
|
|
151
|
-
return t.ASSETS.fetch(
|
|
152
|
-
const
|
|
153
|
-
let
|
|
154
|
-
const
|
|
204
|
+
async fetch(n, t) {
|
|
205
|
+
const e = new URL(n.url), s = O(e, t, o.defaultLocale);
|
|
206
|
+
if (s) return s;
|
|
207
|
+
if (L.test(e.pathname))
|
|
208
|
+
return t.ASSETS.fetch(n);
|
|
209
|
+
const a = R(n, o), l = new URL("/index.html", n.url);
|
|
210
|
+
let i = await (await t.ASSETS.fetch(new Request(l))).text();
|
|
211
|
+
const g = o.defaultLocale, [w, v] = await Promise.all([
|
|
155
212
|
t.SITE_CONTENT.get("config"),
|
|
156
|
-
t.SITE_CONTENT.get(`content:${
|
|
157
|
-
]),
|
|
158
|
-
let
|
|
159
|
-
if (!
|
|
160
|
-
const
|
|
161
|
-
|
|
213
|
+
t.SITE_CONTENT.get(`content:${a}`)
|
|
214
|
+
]), T = w ? JSON.parse(w) : {};
|
|
215
|
+
let p = v ? JSON.parse(v) : {};
|
|
216
|
+
if (!v && a !== g) {
|
|
217
|
+
const c = await t.SITE_CONTENT.get(`content:${g}`);
|
|
218
|
+
c && (p = JSON.parse(c));
|
|
162
219
|
}
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
const
|
|
166
|
-
if (!
|
|
167
|
-
let
|
|
168
|
-
!
|
|
220
|
+
const h = e.pathname.match(/^\/p\/(.+)$/);
|
|
221
|
+
if (h) {
|
|
222
|
+
const c = h[1];
|
|
223
|
+
if (!p[c]) {
|
|
224
|
+
let d = await t.SITE_CONTENT.get(`page:${c}:${a}`);
|
|
225
|
+
!d && a !== g && (d = await t.SITE_CONTENT.get(`page:${c}:${g}`)), d && (p[c] = JSON.parse(d));
|
|
169
226
|
}
|
|
170
227
|
}
|
|
171
|
-
const
|
|
172
|
-
locale:
|
|
173
|
-
config:
|
|
228
|
+
const r = {
|
|
229
|
+
locale: a,
|
|
230
|
+
config: T
|
|
174
231
|
};
|
|
175
|
-
for (const [
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
const
|
|
179
|
-
|
|
232
|
+
for (const [c, d] of Object.entries(p))
|
|
233
|
+
r[`page:${c}`] = d;
|
|
234
|
+
if (h) {
|
|
235
|
+
const c = h[1], d = r[`page:${c}`];
|
|
236
|
+
d && (i = $(i, d, o.siteName));
|
|
180
237
|
}
|
|
181
|
-
e.pathname === "/" &&
|
|
182
|
-
const
|
|
183
|
-
return
|
|
184
|
-
</head>`), new Response(
|
|
238
|
+
e.pathname === "/" && p.home && (i = $(i, p.home, o.siteName));
|
|
239
|
+
const m = `<script>window.__SITE_DATA__ = ${JSON.stringify(r)};<\/script>`;
|
|
240
|
+
return i = i.replace("</head>", `${m}
|
|
241
|
+
</head>`), new Response(i, {
|
|
185
242
|
headers: {
|
|
186
243
|
"Content-Type": "text/html;charset=utf-8",
|
|
187
244
|
"Cache-Control": "public, max-age=60"
|
|
@@ -190,15 +247,15 @@ function J(n) {
|
|
|
190
247
|
}
|
|
191
248
|
};
|
|
192
249
|
}
|
|
193
|
-
const M = /* @__PURE__ */ Symbol("pageContext");
|
|
194
250
|
export {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
251
|
+
I as PAGE_CONTEXT_KEY,
|
|
252
|
+
W as createSiteWorker,
|
|
253
|
+
E as isInPreview,
|
|
254
|
+
z as setupPreviewRouter,
|
|
255
|
+
P as useLivePreview,
|
|
256
|
+
K as usePageData,
|
|
257
|
+
J as useSiteApi,
|
|
258
|
+
M as useSiteConfig,
|
|
259
|
+
k as useSiteData,
|
|
260
|
+
U as useSkin
|
|
204
261
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { ref, computed, watch, onMounted, provide, type Ref } from 'vue';
|
|
2
|
+
import { useI18n } from 'vue-i18n';
|
|
3
|
+
|
|
4
|
+
import type { Block } from '../types/blocks.js';
|
|
5
|
+
import type { PageConfig, PageContext, SiteData } from '../types/config.js';
|
|
6
|
+
import { PAGE_CONTEXT_KEY } from '../types/config.js';
|
|
7
|
+
import { useLivePreview } from './useLivePreview.js';
|
|
8
|
+
|
|
9
|
+
export interface UsePageDataReturn {
|
|
10
|
+
/** Resolved page data (from preload, API, or preview) */
|
|
11
|
+
page: Ref<PageConfig | null>;
|
|
12
|
+
/** Blocks to render (preview-aware) */
|
|
13
|
+
blocks: Ref<Block[]>;
|
|
14
|
+
/** Page context (metadata without blocks) */
|
|
15
|
+
pageContext: Ref<PageContext>;
|
|
16
|
+
/** True while fetching from API */
|
|
17
|
+
loading: Ref<boolean>;
|
|
18
|
+
/** True if page was not found */
|
|
19
|
+
notFound: Ref<boolean>;
|
|
20
|
+
/** True if in live preview mode */
|
|
21
|
+
isPreviewMode: Ref<boolean>;
|
|
22
|
+
/** Reload page data from API */
|
|
23
|
+
reload: () => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Composable for loading page data with preloaded data, API fallback,
|
|
28
|
+
* locale switching, live preview support, and meta tag injection.
|
|
29
|
+
*
|
|
30
|
+
* @param slug - Reactive slug ref or static string (e.g. 'home', 'pricing')
|
|
31
|
+
*/
|
|
32
|
+
export function usePageData(slug: Ref<string> | string): UsePageDataReturn {
|
|
33
|
+
const { locale } = useI18n();
|
|
34
|
+
const { previewPage, isPreviewMode } = useLivePreview();
|
|
35
|
+
|
|
36
|
+
const siteData = (window as any).__SITE_DATA__ as SiteData | undefined;
|
|
37
|
+
|
|
38
|
+
const page = ref<PageConfig | null>(null);
|
|
39
|
+
const loading = ref(true);
|
|
40
|
+
const notFound = ref(false);
|
|
41
|
+
|
|
42
|
+
const resolvedSlug = computed(() => typeof slug === 'string' ? slug : slug.value);
|
|
43
|
+
const pageKey = computed(() => `page:${resolvedSlug.value}`);
|
|
44
|
+
|
|
45
|
+
// Page context: page metadata without blocks (for injection)
|
|
46
|
+
const pageContext = computed<PageContext>(() => {
|
|
47
|
+
const p = isPreviewMode.value ? previewPage.value : page.value;
|
|
48
|
+
if (!p) return {};
|
|
49
|
+
const { blocks, meta, slug: _s, locale: _l, ...rest } = p as any;
|
|
50
|
+
return rest;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Provide page context for child components
|
|
54
|
+
provide(PAGE_CONTEXT_KEY, pageContext);
|
|
55
|
+
|
|
56
|
+
// Blocks: prefer preview data when preview targets this slug
|
|
57
|
+
const blocks = computed<Block[]>(() => {
|
|
58
|
+
if (isPreviewMode.value && previewPage.value?.slug === resolvedSlug.value) {
|
|
59
|
+
return (previewPage.value.blocks || []) as Block[];
|
|
60
|
+
}
|
|
61
|
+
return page.value?.blocks || [];
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function loadFromPreloaded(): boolean {
|
|
65
|
+
const preloaded = siteData?.[pageKey.value];
|
|
66
|
+
if (preloaded && locale.value === siteData?.locale) {
|
|
67
|
+
page.value = preloaded as PageConfig;
|
|
68
|
+
notFound.value = false;
|
|
69
|
+
applyMeta();
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function fetchPage() {
|
|
76
|
+
loading.value = true;
|
|
77
|
+
notFound.value = false;
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch(`/api/content/${pageKey.value}:${locale.value}`);
|
|
80
|
+
if (res.ok) {
|
|
81
|
+
page.value = await res.json();
|
|
82
|
+
applyMeta();
|
|
83
|
+
} else {
|
|
84
|
+
page.value = null;
|
|
85
|
+
notFound.value = true;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
page.value = null;
|
|
89
|
+
notFound.value = true;
|
|
90
|
+
} finally {
|
|
91
|
+
loading.value = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function applyMeta() {
|
|
96
|
+
if (!page.value) return;
|
|
97
|
+
if (page.value.title) {
|
|
98
|
+
const branding = siteData?.config?.branding;
|
|
99
|
+
const suffix = branding?.siteName ? ` — ${branding.siteName}` : '';
|
|
100
|
+
document.title = `${page.value.title}${suffix}`;
|
|
101
|
+
}
|
|
102
|
+
const desc = page.value.meta?.description;
|
|
103
|
+
if (desc) {
|
|
104
|
+
const el = document.querySelector('meta[name="description"]');
|
|
105
|
+
if (el) el.setAttribute('content', desc);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Live preview: merge incoming page data
|
|
110
|
+
watch(previewPage, (p) => {
|
|
111
|
+
if (p?.slug === resolvedSlug.value) {
|
|
112
|
+
loading.value = false;
|
|
113
|
+
notFound.value = false;
|
|
114
|
+
if (p.title || p.blocks) {
|
|
115
|
+
page.value = {
|
|
116
|
+
...(page.value || {} as PageConfig),
|
|
117
|
+
...(p.title ? { title: p.title } : {}),
|
|
118
|
+
...(p.blocks ? { blocks: p.blocks as Block[] } : {}),
|
|
119
|
+
};
|
|
120
|
+
applyMeta();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Initial load
|
|
126
|
+
onMounted(() => {
|
|
127
|
+
if (!loadFromPreloaded()) {
|
|
128
|
+
fetchPage();
|
|
129
|
+
} else {
|
|
130
|
+
loading.value = false;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Refetch on locale or slug change
|
|
135
|
+
watch(locale, () => fetchPage());
|
|
136
|
+
watch(resolvedSlug, () => {
|
|
137
|
+
if (!loadFromPreloaded()) {
|
|
138
|
+
fetchPage();
|
|
139
|
+
} else {
|
|
140
|
+
loading.value = false;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
page,
|
|
146
|
+
blocks,
|
|
147
|
+
pageContext,
|
|
148
|
+
loading,
|
|
149
|
+
notFound,
|
|
150
|
+
isPreviewMode,
|
|
151
|
+
reload: fetchPage,
|
|
152
|
+
};
|
|
153
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { useSiteConfig } from './composables/useSiteConfig.js';
|
|
|
4
4
|
export { useSkin } from './composables/useSkin.js';
|
|
5
5
|
export { useSiteApi } from './composables/useSiteApi.js';
|
|
6
6
|
export { useLivePreview, setupPreviewRouter, isInPreview } from './composables/useLivePreview.js';
|
|
7
|
+
export { usePageData } from './composables/usePageData.js';
|
|
8
|
+
export type { UsePageDataReturn } from './composables/usePageData.js';
|
|
7
9
|
export type { PreviewMessage } from './composables/useLivePreview.js';
|
|
8
10
|
|
|
9
11
|
// Worker
|