@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.
@@ -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 T, watch as $, computed as u, onUnmounted as b } from "vue";
2
- import { useI18n as C } from "vue-i18n";
3
- function R(n, o) {
4
- const t = y(null), { locale: e } = C(), a = window.__SITE_DATA__, s = a?.locale;
5
- function c() {
6
- return a && a[n] && e.value === s ? (t.value = a[n], !0) : !1;
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 h() {
8
+ async function u() {
9
9
  try {
10
- const r = await fetch(`/api/content/${o}:${e.value}`);
11
- r.ok && (t.value = await r.json());
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 T(() => {
16
- c() || h();
17
- }), $(e, () => {
18
- h();
15
+ return S(() => {
16
+ l() || u();
17
+ }), _(e, () => {
18
+ u();
19
19
  }), t;
20
20
  }
21
- function O() {
22
- const n = window.__SITE_DATA__, o = u(() => n?.config || null), t = u(() => n?.components || {}), e = u(() => o.value?.navigation || []), a = u(() => o.value?.locales || []), s = u(() => o.value?.defaultLocale || "en"), c = u(() => o.value?.branding || {}), h = u(() => o.value?.footer || {}), r = u(() => o.value?.features || {});
23
- function f(g) {
24
- const p = r.value[g];
25
- return typeof p == "boolean" ? p : typeof p == "object";
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: o,
28
+ config: n,
29
29
  components: t,
30
30
  navigation: e,
31
- locales: a,
32
- defaultLocale: s,
33
- branding: c,
34
- footer: h,
35
- features: r,
36
- hasFeature: f
31
+ locales: s,
32
+ defaultLocale: a,
33
+ branding: l,
34
+ footer: u,
35
+ features: i,
36
+ hasFeature: g
37
37
  };
38
38
  }
39
- function j(n) {
40
- function o(t) {
39
+ function U(o) {
40
+ function n(t) {
41
41
  const e = document.documentElement;
42
42
  if (t.colors)
43
- for (const [a, s] of Object.entries(t.colors))
44
- s && e.style.setProperty(`--x-color-${x(a)}`, s);
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
- T(() => {
48
- if (n) {
49
- o(n);
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) && o({
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 x(n) {
61
- return n.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
60
+ function N(o) {
61
+ return o.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
62
62
  }
63
- function F() {
64
- const o = window.__SITE_DATA__?.apiBase || "";
65
- async function t(s) {
66
- const c = await fetch(`${o}${s}`);
67
- if (!c.ok)
68
- throw new Error(`API error: ${c.status}`);
69
- return c.json();
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 a() {
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: a
80
+ getProducts: s
81
81
  };
82
82
  }
83
- function S() {
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 D(n) {
91
- S() && n.beforeEach((o, t) => !t.name);
90
+ function z(o) {
91
+ E() && o.beforeEach((n, t) => !t.name);
92
92
  }
93
- function U() {
94
- const n = y(null), o = y(S());
93
+ function P() {
94
+ const o = y(null), n = y(E());
95
95
  function t(e) {
96
- const a = e.data;
97
- a?.type === "xosen-preview-update" && a.page && (n.value = a.page, o.value = !0);
96
+ const s = e.data;
97
+ s?.type === "xosen-preview-update" && s.page && (o.value = s.page, n.value = !0);
98
98
  }
99
- return T(() => {
99
+ return S(() => {
100
100
  window.addEventListener("message", t);
101
- }), b(() => {
101
+ }), x(() => {
102
102
  window.removeEventListener("message", t);
103
103
  }), {
104
- previewPage: n,
105
- isPreviewMode: o
104
+ previewPage: o,
105
+ isPreviewMode: n
106
106
  };
107
107
  }
108
- const A = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|webp|avif|map|json|txt|xml|webmanifest)$/;
109
- function N(n, o) {
110
- const e = new URL(n.url).searchParams.get("lang");
111
- if (e && o.supportedLocales?.includes(e)) return e;
112
- if (o.supportedLocales?.length) {
113
- const a = n.headers.get("Accept-Language") || "";
114
- for (const s of o.supportedLocales)
115
- if (a.includes(s)) return s;
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 o.defaultLocale;
174
+ return n.defaultLocale;
118
175
  }
119
- function I(n, o, t) {
120
- if (!n.pathname.startsWith("/api/content/")) return null;
121
- const e = decodeURIComponent(n.pathname.replace("/api/content/", ""));
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 a = await o.SITE_CONTENT.get(e);
124
- if (!a) {
125
- const s = e.lastIndexOf(":");
126
- s > 0 && e.slice(s + 1) !== t && (a = await o.SITE_CONTENT.get(e.slice(0, s + 1) + t));
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 a ? new Response(a, {
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 _(n, o, t) {
138
- let e = n;
139
- if (o.title) {
140
- const a = t ? ` — ${t}` : "";
141
- e = e.replace(/<title>[^<]*<\/title>/, `<title>${o.title}${a}</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 o.meta?.description && (e = e.replace(/(<meta\s+name="description"\s+content=")[^"]*(")/, `$1${o.meta.description}$2`)), o.meta?.ogTitle && (e = e.replace(/(<meta\s+property="og:title"\s+content=")[^"]*(")/, `$1${o.meta.ogTitle}$2`)), o.html && (e = e.replace('<div id="app"></div>', `<div id="app">${o.html}</div>`)), e;
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 J(n) {
202
+ function W(o) {
146
203
  return {
147
- async fetch(o, t) {
148
- const e = new URL(o.url), a = I(e, t, n.defaultLocale);
149
- if (a) return a;
150
- if (A.test(e.pathname))
151
- return t.ASSETS.fetch(o);
152
- const s = N(o, n), c = new URL("/index.html", o.url);
153
- let r = await (await t.ASSETS.fetch(new Request(c))).text();
154
- const f = n.defaultLocale, [g, p] = await Promise.all([
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:${s}`)
157
- ]), v = g ? JSON.parse(g) : {};
158
- let d = p ? JSON.parse(p) : {};
159
- if (!p && s !== f) {
160
- const i = await t.SITE_CONTENT.get(`content:${f}`);
161
- i && (d = JSON.parse(i));
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 m = e.pathname.match(/^\/p\/(.+)$/);
164
- if (m) {
165
- const i = m[1];
166
- if (!d[i]) {
167
- let l = await t.SITE_CONTENT.get(`page:${i}:${s}`);
168
- !l && s !== f && (l = await t.SITE_CONTENT.get(`page:${i}:${f}`)), l && (d[i] = JSON.parse(l));
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 w = {
172
- locale: s,
173
- config: v
228
+ const r = {
229
+ locale: a,
230
+ config: T
174
231
  };
175
- for (const [i, l] of Object.entries(d))
176
- w[`page:${i}`] = l;
177
- if (m) {
178
- const i = m[1], l = w[`page:${i}`];
179
- l && (r = _(r, l, n.siteName));
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 === "/" && d.home && (r = _(r, d.home, n.siteName));
182
- const E = `<script>window.__SITE_DATA__ = ${JSON.stringify(w)};<\/script>`;
183
- return r = r.replace("</head>", `${E}
184
- </head>`), new Response(r, {
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
- M as PAGE_CONTEXT_KEY,
196
- J as createSiteWorker,
197
- S as isInPreview,
198
- D as setupPreviewRouter,
199
- U as useLivePreview,
200
- F as useSiteApi,
201
- O as useSiteConfig,
202
- R as useSiteData,
203
- j as useSkin
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xosen/site-sdk",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "Shared Vue components and composables for Xosen site templates",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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