@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.
- package/dist/composables/useLocaleSwitcher.d.ts +12 -0
- package/dist/composables/usePageData.d.ts +26 -0
- package/dist/composables/useSiteComponents.d.ts +15 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +221 -117
- package/package.json +1 -1
- package/src/composables/useLocaleSwitcher.ts +40 -0
- package/src/composables/usePageData.ts +153 -0
- package/src/composables/useSiteComponents.ts +59 -0
- package/src/index.ts +4 -0
|
@@ -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
|
|
2
|
-
import { useI18n as
|
|
3
|
-
function
|
|
4
|
-
const t = y(null), { locale: e } =
|
|
5
|
-
function
|
|
6
|
-
return
|
|
7
|
-
}
|
|
8
|
-
async function
|
|
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
|
|
11
|
-
|
|
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
|
-
|
|
17
|
-
}),
|
|
18
|
-
|
|
16
|
+
l() || i();
|
|
17
|
+
}), _(e, () => {
|
|
18
|
+
i();
|
|
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 = 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:
|
|
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: i,
|
|
35
|
+
features: c,
|
|
36
|
+
hasFeature: v
|
|
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
47
|
T(() => {
|
|
48
|
-
if (
|
|
49
|
-
o
|
|
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
99
|
return T(() => {
|
|
100
100
|
window.addEventListener("message", t);
|
|
101
|
-
}),
|
|
101
|
+
}), C(() => {
|
|
102
102
|
window.removeEventListener("message", t);
|
|
103
103
|
}), {
|
|
104
|
-
previewPage:
|
|
105
|
-
isPreviewMode:
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
120
|
-
if (!
|
|
121
|
-
const e = decodeURIComponent(
|
|
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
|
|
124
|
-
if (!
|
|
125
|
-
const
|
|
126
|
-
|
|
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
|
|
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
|
|
138
|
-
let e =
|
|
139
|
-
if (
|
|
140
|
-
const
|
|
141
|
-
e = e.replace(/<title>[^<]*<\/title>/, `<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
|
|
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
|
|
247
|
+
function X(o) {
|
|
146
248
|
return {
|
|
147
|
-
async fetch(
|
|
148
|
-
const e = new URL(
|
|
149
|
-
if (
|
|
150
|
-
if (
|
|
151
|
-
return t.ASSETS.fetch(
|
|
152
|
-
const
|
|
153
|
-
let
|
|
154
|
-
const
|
|
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:${
|
|
157
|
-
]),
|
|
158
|
-
let
|
|
159
|
-
if (!
|
|
160
|
-
const
|
|
161
|
-
|
|
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
|
|
166
|
-
if (!
|
|
167
|
-
let
|
|
168
|
-
!
|
|
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
|
|
172
|
-
locale:
|
|
173
|
-
config:
|
|
273
|
+
const r = {
|
|
274
|
+
locale: a,
|
|
275
|
+
config: S
|
|
174
276
|
};
|
|
175
|
-
for (const [
|
|
176
|
-
|
|
277
|
+
for (const [f, d] of Object.entries(p))
|
|
278
|
+
r[`page:${f}`] = d;
|
|
177
279
|
if (m) {
|
|
178
|
-
const
|
|
179
|
-
|
|
280
|
+
const f = m[1], d = r[`page:${f}`];
|
|
281
|
+
d && (c = b(c, d, o.siteName));
|
|
180
282
|
}
|
|
181
|
-
e.pathname === "/" &&
|
|
182
|
-
const
|
|
183
|
-
return
|
|
184
|
-
</head>`), new Response(
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
@@ -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
|