@xosen/site-sdk 0.0.9 → 0.0.11

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,12 @@
1
+ /**
2
+ * Locale switcher composable.
3
+ * Manages locale state, available locales, and persistence.
4
+ */
5
+ export declare function useLocaleSwitcher(): {
6
+ locale: import("vue").WritableComputedRef<string, string>;
7
+ locales: import("vue").ComputedRef<string[]>;
8
+ defaultLocale: import("vue").ComputedRef<string>;
9
+ hasMultipleLocales: import("vue").ComputedRef<boolean>;
10
+ switchLocale: (loc: string) => void;
11
+ resolveText: (text: string | Record<string, string>) => string;
12
+ };
@@ -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;
@@ -0,0 +1,15 @@
1
+ import { type Ref } from 'vue';
2
+ /**
3
+ * Locale-aware access to site components (navigation, footer, sidebar, etc.).
4
+ *
5
+ * On initial load, reads from preloaded __SITE_DATA__.components.
6
+ * When locale changes, fetches updated components from the API.
7
+ */
8
+ export declare function useSiteComponents(): {
9
+ /** All components (reactive, locale-aware) */
10
+ components: Ref<Record<string, any>, Record<string, any>>;
11
+ /** Get a single component by key */
12
+ getComponent: <T = any>(key: string) => Ref<T | null>;
13
+ /** Force refetch for current locale */
14
+ reload: () => Promise<void>;
15
+ };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,10 @@ 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';
8
+ export { useSiteComponents } from './composables/useSiteComponents.js';
9
+ export { useLocaleSwitcher } from './composables/useLocaleSwitcher.js';
6
10
  export type { PreviewMessage } from './composables/useLivePreview.js';
7
11
  export { createSiteWorker } from './worker/index.js';
8
12
  export type { WorkerEnv, WorkerConfig, PageData } from './worker/index.js';
package/dist/index.js CHANGED
@@ -1,131 +1,233 @@
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;
7
- }
8
- async function h() {
1
+ import { ref as y, onMounted as T, watch as _, computed as u, onUnmounted as C, provide as L } from "vue";
2
+ import { useI18n as $ } from "vue-i18n";
3
+ function k(o, n) {
4
+ const t = y(null), { locale: e } = $(), 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
+ }
8
+ async function i() {
9
9
  try {
10
- const r = await fetch(`/api/content/${o}:${e.value}`);
11
- r.ok && (t.value = await r.json());
10
+ const c = await fetch(`/api/content/${n}:${e.value}`);
11
+ c.ok && (t.value = await c.json());
12
12
  } catch {
13
13
  }
14
14
  }
15
15
  return T(() => {
16
- c() || h();
17
- }), $(e, () => {
18
- h();
16
+ l() || i();
17
+ }), _(e, () => {
18
+ i();
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 = u(() => o?.config || null), t = u(() => o?.components || {}), e = u(() => n.value?.navigation || []), s = u(() => n.value?.locales || []), a = u(() => n.value?.defaultLocale || "en"), l = u(() => n.value?.branding || {}), i = u(() => n.value?.footer || {}), c = u(() => n.value?.features || {});
23
+ function v(w) {
24
+ const g = c.value[w];
25
+ return typeof g == "boolean" ? g : typeof g == "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: i,
35
+ features: c,
36
+ hasFeature: v
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
47
  T(() => {
48
- if (n) {
49
- o(n);
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
99
  return T(() => {
100
100
  window.addEventListener("message", t);
101
- }), b(() => {
101
+ }), C(() => {
102
102
  window.removeEventListener("message", t);
103
103
  }), {
104
- previewPage: n,
105
- isPreviewMode: o
104
+ previewPage: o,
105
+ isPreviewMode: n
106
+ };
107
+ }
108
+ const x = /* @__PURE__ */ Symbol("pageContext");
109
+ function K(o) {
110
+ const { locale: n } = $(), { previewPage: t, isPreviewMode: e } = P(), s = window.__SITE_DATA__, a = y(null), l = y(!0), i = y(!1), c = u(() => typeof o == "string" ? o : o.value), v = u(() => `page:${c.value}`), w = u(() => {
111
+ const r = e.value ? t.value : a.value;
112
+ if (!r) return {};
113
+ const { blocks: h, meta: f, slug: d, locale: O, ...A } = r;
114
+ return A;
115
+ });
116
+ L(x, w);
117
+ const g = u(() => e.value && t.value?.slug === c.value ? t.value.blocks || [] : a.value?.blocks || []);
118
+ function S() {
119
+ const r = s?.[v.value];
120
+ return r && n.value === s?.locale ? (a.value = r, i.value = !1, m(), !0) : !1;
121
+ }
122
+ async function p() {
123
+ l.value = !0, i.value = !1;
124
+ try {
125
+ const r = await fetch(`/api/content/${v.value}:${n.value}`);
126
+ r.ok ? (a.value = await r.json(), m()) : (a.value = null, i.value = !0);
127
+ } catch {
128
+ a.value = null, i.value = !0;
129
+ } finally {
130
+ l.value = !1;
131
+ }
132
+ }
133
+ function m() {
134
+ if (!a.value) return;
135
+ if (a.value.title) {
136
+ const h = s?.config?.branding, f = h?.siteName ? ` — ${h.siteName}` : "";
137
+ document.title = `${a.value.title}${f}`;
138
+ }
139
+ const r = a.value.meta?.description;
140
+ if (r) {
141
+ const h = document.querySelector('meta[name="description"]');
142
+ h && h.setAttribute("content", r);
143
+ }
144
+ }
145
+ return _(t, (r) => {
146
+ r?.slug === c.value && (l.value = !1, i.value = !1, (r.title || r.blocks) && (a.value = {
147
+ ...a.value || {},
148
+ ...r.title ? { title: r.title } : {},
149
+ ...r.blocks ? { blocks: r.blocks } : {}
150
+ }, m()));
151
+ }), T(() => {
152
+ S() ? l.value = !1 : p();
153
+ }), _(n, () => p()), _(c, () => {
154
+ S() ? l.value = !1 : p();
155
+ }), {
156
+ page: a,
157
+ blocks: g,
158
+ pageContext: w,
159
+ loading: l,
160
+ notFound: i,
161
+ isPreviewMode: e,
162
+ reload: p
163
+ };
164
+ }
165
+ function W() {
166
+ const { locale: o } = $(), n = window.__SITE_DATA__, t = n?.locale, e = y({ ...n?.components || {} });
167
+ async function s(l) {
168
+ if (l === t) {
169
+ e.value = { ...n?.components || {} };
170
+ return;
171
+ }
172
+ try {
173
+ const i = await fetch(`/api/content/components:${l}`);
174
+ i.ok && (e.value = await i.json());
175
+ } catch {
176
+ }
177
+ }
178
+ _(o, (l) => s(l)), T(() => {
179
+ o.value !== t && s(o.value);
180
+ });
181
+ function a(l) {
182
+ return u(() => e.value[l] || null);
183
+ }
184
+ return {
185
+ /** All components (reactive, locale-aware) */
186
+ components: e,
187
+ /** Get a single component by key */
188
+ getComponent: a,
189
+ /** Force refetch for current locale */
190
+ reload: () => s(o.value)
106
191
  };
107
192
  }
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;
116
- }
117
- return o.defaultLocale;
193
+ function B() {
194
+ const { locale: o } = $(), n = window.__SITE_DATA__, t = u(() => n?.config?.locales || []), e = u(() => n?.config?.defaultLocale || "en"), s = u(() => t.value.length > 1);
195
+ function a(i) {
196
+ o.value = i, localStorage.setItem("x-site-locale", i);
197
+ }
198
+ function l(i) {
199
+ return typeof i == "string" ? i : i[o.value] || i[e.value] || Object.values(i)[0] || "";
200
+ }
201
+ return {
202
+ locale: o,
203
+ locales: t,
204
+ defaultLocale: e,
205
+ hasMultipleLocales: s,
206
+ switchLocale: a,
207
+ resolveText: l
208
+ };
209
+ }
210
+ const I = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|webp|avif|map|json|txt|xml|webmanifest)$/;
211
+ function R(o, n) {
212
+ const e = new URL(o.url).searchParams.get("lang");
213
+ if (e && n.supportedLocales?.includes(e)) return e;
214
+ if (n.supportedLocales?.length) {
215
+ const s = o.headers.get("Accept-Language") || "";
216
+ for (const a of n.supportedLocales)
217
+ if (s.includes(a)) return a;
218
+ }
219
+ return n.defaultLocale;
118
220
  }
119
- function I(n, o, t) {
120
- if (!n.pathname.startsWith("/api/content/")) return null;
121
- const e = decodeURIComponent(n.pathname.replace("/api/content/", ""));
221
+ function j(o, n, t) {
222
+ if (!o.pathname.startsWith("/api/content/")) return null;
223
+ const e = decodeURIComponent(o.pathname.replace("/api/content/", ""));
122
224
  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));
225
+ let s = await n.SITE_CONTENT.get(e);
226
+ if (!s) {
227
+ const a = e.lastIndexOf(":");
228
+ a > 0 && e.slice(a + 1) !== t && (s = await n.SITE_CONTENT.get(e.slice(0, a + 1) + t));
127
229
  }
128
- return a ? new Response(a, {
230
+ return s ? new Response(s, {
129
231
  headers: {
130
232
  "Content-Type": "application/json",
131
233
  "Cache-Control": "public, max-age=60",
@@ -134,54 +236,54 @@ function I(n, o, t) {
134
236
  }) : Response.json({ error: "Not found" }, { status: 404 });
135
237
  })() : Promise.resolve(Response.json({ error: "Key is required" }, { status: 400 }));
136
238
  }
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>`);
239
+ function b(o, n, t) {
240
+ let e = o;
241
+ if (n.title) {
242
+ const s = t ? ` — ${t}` : "";
243
+ e = e.replace(/<title>[^<]*<\/title>/, `<title>${n.title}${s}</title>`);
142
244
  }
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;
245
+ 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
246
  }
145
- function J(n) {
247
+ function X(o) {
146
248
  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([
249
+ async fetch(n, t) {
250
+ const e = new URL(n.url), s = j(e, t, o.defaultLocale);
251
+ if (s) return s;
252
+ if (I.test(e.pathname))
253
+ return t.ASSETS.fetch(n);
254
+ const a = R(n, o), l = new URL("/index.html", n.url);
255
+ let c = await (await t.ASSETS.fetch(new Request(l))).text();
256
+ const v = o.defaultLocale, [w, g] = await Promise.all([
155
257
  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));
258
+ t.SITE_CONTENT.get(`content:${a}`)
259
+ ]), S = w ? JSON.parse(w) : {};
260
+ let p = g ? JSON.parse(g) : {};
261
+ if (!g && a !== v) {
262
+ const f = await t.SITE_CONTENT.get(`content:${v}`);
263
+ f && (p = JSON.parse(f));
162
264
  }
163
265
  const m = e.pathname.match(/^\/p\/(.+)$/);
164
266
  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));
267
+ const f = m[1];
268
+ if (!p[f]) {
269
+ let d = await t.SITE_CONTENT.get(`page:${f}:${a}`);
270
+ !d && a !== v && (d = await t.SITE_CONTENT.get(`page:${f}:${v}`)), d && (p[f] = JSON.parse(d));
169
271
  }
170
272
  }
171
- const w = {
172
- locale: s,
173
- config: v
273
+ const r = {
274
+ locale: a,
275
+ config: S
174
276
  };
175
- for (const [i, l] of Object.entries(d))
176
- w[`page:${i}`] = l;
277
+ for (const [f, d] of Object.entries(p))
278
+ r[`page:${f}`] = d;
177
279
  if (m) {
178
- const i = m[1], l = w[`page:${i}`];
179
- l && (r = _(r, l, n.siteName));
280
+ const f = m[1], d = r[`page:${f}`];
281
+ d && (c = b(c, d, o.siteName));
180
282
  }
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, {
283
+ e.pathname === "/" && p.home && (c = b(c, p.home, o.siteName));
284
+ const h = `<script>window.__SITE_DATA__ = ${JSON.stringify(r)};<\/script>`;
285
+ return c = c.replace("</head>", `${h}
286
+ </head>`), new Response(c, {
185
287
  headers: {
186
288
  "Content-Type": "text/html;charset=utf-8",
187
289
  "Cache-Control": "public, max-age=60"
@@ -190,15 +292,17 @@ function J(n) {
190
292
  }
191
293
  };
192
294
  }
193
- const M = /* @__PURE__ */ Symbol("pageContext");
194
295
  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
296
+ x as PAGE_CONTEXT_KEY,
297
+ X as createSiteWorker,
298
+ E as isInPreview,
299
+ z as setupPreviewRouter,
300
+ P as useLivePreview,
301
+ B as useLocaleSwitcher,
302
+ K as usePageData,
303
+ J as useSiteApi,
304
+ W as useSiteComponents,
305
+ M as useSiteConfig,
306
+ k as useSiteData,
307
+ U as useSkin
204
308
  };
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.11",
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,40 @@
1
+ import { computed } from 'vue';
2
+ import { useI18n } from 'vue-i18n';
3
+
4
+ import type { SiteData } from '../types/config.js';
5
+
6
+ /**
7
+ * Locale switcher composable.
8
+ * Manages locale state, available locales, and persistence.
9
+ */
10
+ export function useLocaleSwitcher() {
11
+ const { locale } = useI18n();
12
+ const siteData = (window as any).__SITE_DATA__ as SiteData | undefined;
13
+
14
+ const locales = computed(() => siteData?.config?.locales || []);
15
+ const defaultLocale = computed(() => siteData?.config?.defaultLocale || 'en');
16
+ const hasMultipleLocales = computed(() => locales.value.length > 1);
17
+
18
+ function switchLocale(loc: string) {
19
+ locale.value = loc;
20
+ localStorage.setItem('x-site-locale', loc);
21
+ }
22
+
23
+ /**
24
+ * Resolve a text value that may be a string or a locale map.
25
+ * e.g. "Home" or { "en": "Home", "uk": "Головна" }
26
+ */
27
+ function resolveText(text: string | Record<string, string>): string {
28
+ if (typeof text === 'string') return text;
29
+ return text[locale.value] || text[defaultLocale.value] || Object.values(text)[0] || '';
30
+ }
31
+
32
+ return {
33
+ locale,
34
+ locales,
35
+ defaultLocale,
36
+ hasMultipleLocales,
37
+ switchLocale,
38
+ resolveText,
39
+ };
40
+ }
@@ -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
+ }
@@ -0,0 +1,59 @@
1
+ import { ref, computed, watch, onMounted, type Ref } from 'vue';
2
+ import { useI18n } from 'vue-i18n';
3
+
4
+ import type { SiteData } from '../types/config.js';
5
+
6
+ /**
7
+ * Locale-aware access to site components (navigation, footer, sidebar, etc.).
8
+ *
9
+ * On initial load, reads from preloaded __SITE_DATA__.components.
10
+ * When locale changes, fetches updated components from the API.
11
+ */
12
+ export function useSiteComponents() {
13
+ const { locale } = useI18n();
14
+ const siteData = (window as any).__SITE_DATA__ as SiteData | undefined;
15
+ const initialLocale = siteData?.locale;
16
+
17
+ const components = ref<Record<string, any>>({ ...(siteData as any)?.components || {} });
18
+
19
+ async function fetchComponents(loc: string) {
20
+ // If same as initial locale, use preloaded data
21
+ if (loc === initialLocale) {
22
+ components.value = { ...(siteData as any)?.components || {} };
23
+ return;
24
+ }
25
+ try {
26
+ const res = await fetch(`/api/content/components:${loc}`);
27
+ if (res.ok) {
28
+ components.value = await res.json();
29
+ }
30
+ } catch {
31
+ // Keep current
32
+ }
33
+ }
34
+
35
+ watch(locale, (newLocale) => fetchComponents(newLocale));
36
+
37
+ onMounted(() => {
38
+ if (locale.value !== initialLocale) {
39
+ fetchComponents(locale.value);
40
+ }
41
+ });
42
+
43
+ /**
44
+ * Get a specific component's data by key.
45
+ * Returns a computed ref that updates on locale change.
46
+ */
47
+ function getComponent<T = any>(key: string): Ref<T | null> {
48
+ return computed(() => components.value[key] || null) as Ref<T | null>;
49
+ }
50
+
51
+ return {
52
+ /** All components (reactive, locale-aware) */
53
+ components,
54
+ /** Get a single component by key */
55
+ getComponent,
56
+ /** Force refetch for current locale */
57
+ reload: () => fetchComponents(locale.value),
58
+ };
59
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,10 @@ 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';
9
+ export { useSiteComponents } from './composables/useSiteComponents.js';
10
+ export { useLocaleSwitcher } from './composables/useLocaleSwitcher.js';
7
11
  export type { PreviewMessage } from './composables/useLivePreview.js';
8
12
 
9
13
  // Worker