@xosen/site-sdk 0.0.8 → 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/useLivePreview.d.ts +54 -0
- package/dist/composables/usePageData.d.ts +26 -0
- package/dist/composables/useSiteApi.d.ts +9 -0
- package/dist/composables/useSiteConfig.d.ts +25 -0
- package/dist/composables/useSiteData.d.ts +8 -0
- package/dist/composables/useSkin.d.ts +6 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +178 -121
- package/dist/types/blocks.d.ts +103 -0
- package/dist/types/config.d.ts +56 -0
- package/dist/types/skin.d.ts +41 -0
- package/dist/worker/create-site-worker.d.ts +14 -0
- package/dist/worker/index.d.ts +2 -0
- package/dist/worker/types.d.ts +41 -0
- package/package.json +5 -5
- package/src/composables/usePageData.ts +153 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Router } from 'vue-router';
|
|
2
|
+
export interface PreviewMessage {
|
|
3
|
+
type: 'xosen-preview-update';
|
|
4
|
+
page: {
|
|
5
|
+
slug: string;
|
|
6
|
+
locale: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
blocks?: any[];
|
|
9
|
+
meta?: Record<string, any>;
|
|
10
|
+
layout?: string;
|
|
11
|
+
/** Page-level context fields (author, featuredImage, excerpt, etc.) */
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Check if running inside an iframe (preview mode) */
|
|
16
|
+
export declare function isInPreview(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Install a router guard that blocks all navigation in preview mode.
|
|
19
|
+
* Call this once during app setup (e.g. in App.vue or router config).
|
|
20
|
+
*/
|
|
21
|
+
export declare function setupPreviewRouter(router: Router): void;
|
|
22
|
+
/**
|
|
23
|
+
* Listen for live preview messages from the admin UI.
|
|
24
|
+
* When the admin sends page data via postMessage, this composable
|
|
25
|
+
* provides reactive access to the preview data.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useLivePreview(): {
|
|
28
|
+
previewPage: import("vue").Ref<{
|
|
29
|
+
[x: string]: any;
|
|
30
|
+
slug: string;
|
|
31
|
+
locale: string;
|
|
32
|
+
title?: string | undefined;
|
|
33
|
+
blocks?: any[] | undefined;
|
|
34
|
+
meta?: Record<string, any> | undefined;
|
|
35
|
+
layout?: string | undefined;
|
|
36
|
+
} | null, {
|
|
37
|
+
[key: string]: any;
|
|
38
|
+
slug: string;
|
|
39
|
+
locale: string;
|
|
40
|
+
title?: string;
|
|
41
|
+
blocks?: any[];
|
|
42
|
+
meta?: Record<string, any>;
|
|
43
|
+
layout?: string;
|
|
44
|
+
} | {
|
|
45
|
+
[x: string]: any;
|
|
46
|
+
slug: string;
|
|
47
|
+
locale: string;
|
|
48
|
+
title?: string | undefined;
|
|
49
|
+
blocks?: any[] | undefined;
|
|
50
|
+
meta?: Record<string, any> | undefined;
|
|
51
|
+
layout?: string | undefined;
|
|
52
|
+
} | null>;
|
|
53
|
+
isPreviewMode: import("vue").Ref<boolean, boolean>;
|
|
54
|
+
};
|
|
@@ -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,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch data from the Xosen org API (for live data like tariffs, products).
|
|
3
|
+
* Uses the API base URL from site config.
|
|
4
|
+
*/
|
|
5
|
+
export declare function useSiteApi(): {
|
|
6
|
+
get: <T>(path: string) => Promise<T>;
|
|
7
|
+
getTariffs: () => Promise<any[]>;
|
|
8
|
+
getProducts: () => Promise<any[]>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SiteConfig } from '../types/config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Access the site configuration injected by the CF Worker.
|
|
4
|
+
*/
|
|
5
|
+
export declare function useSiteConfig(): {
|
|
6
|
+
config: import("vue").ComputedRef<SiteConfig | null>;
|
|
7
|
+
components: import("vue").ComputedRef<Record<string, any>>;
|
|
8
|
+
navigation: import("vue").ComputedRef<import("../index.js").NavItem[]>;
|
|
9
|
+
locales: import("vue").ComputedRef<string[]>;
|
|
10
|
+
defaultLocale: import("vue").ComputedRef<string>;
|
|
11
|
+
branding: import("vue").ComputedRef<{
|
|
12
|
+
logo?: string;
|
|
13
|
+
favicon?: string;
|
|
14
|
+
siteName?: string;
|
|
15
|
+
}>;
|
|
16
|
+
footer: import("vue").ComputedRef<{
|
|
17
|
+
copyright?: string;
|
|
18
|
+
links?: Array<{
|
|
19
|
+
text: string;
|
|
20
|
+
url: string;
|
|
21
|
+
}>;
|
|
22
|
+
}>;
|
|
23
|
+
features: import("vue").ComputedRef<Record<string, boolean | Record<string, any>>>;
|
|
24
|
+
hasFeature: (key: string) => boolean;
|
|
25
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Ref } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* Read preloaded site data from window.__SITE_DATA__ or fetch from API.
|
|
4
|
+
*
|
|
5
|
+
* @param key - Key in __SITE_DATA__ object (e.g. 'tariffs', 'contacts:offices')
|
|
6
|
+
* @param kvKey - KV key suffix for API fallback (e.g. 'tariffs', 'contacts:offices')
|
|
7
|
+
*/
|
|
8
|
+
export declare function useSiteData<T>(key: string, kvKey: string): Ref<T | null>;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { useSiteData } from './composables/useSiteData.js';
|
|
2
|
+
export { useSiteConfig } from './composables/useSiteConfig.js';
|
|
3
|
+
export { useSkin } from './composables/useSkin.js';
|
|
4
|
+
export { useSiteApi } from './composables/useSiteApi.js';
|
|
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 type { PreviewMessage } from './composables/useLivePreview.js';
|
|
9
|
+
export { createSiteWorker } from './worker/index.js';
|
|
10
|
+
export type { WorkerEnv, WorkerConfig, PageData } from './worker/index.js';
|
|
11
|
+
export type { Block, BlockSettings, HeroBlockData, HtmlBlockData, CardsBlockData, ImageTextBlockData, PricingBlockData, PricingPlan, ContactsBlockData, Office, GalleryBlockData, MapBlockData, } from './types/blocks.js';
|
|
12
|
+
export type { SiteConfig, NavItem, PageConfig, SiteData, PageContext } from './types/config.js';
|
|
13
|
+
export { PAGE_CONTEXT_KEY } from './types/config.js';
|
|
14
|
+
export type { Skin } from './types/skin.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
|
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export interface BlockSettings {
|
|
2
|
+
id?: string;
|
|
3
|
+
visible?: boolean;
|
|
4
|
+
className?: string;
|
|
5
|
+
spacing?: 'none' | 'sm' | 'md' | 'lg';
|
|
6
|
+
}
|
|
7
|
+
export interface Block<T = any> {
|
|
8
|
+
type: string;
|
|
9
|
+
data?: T;
|
|
10
|
+
/** Alias for data — accepted for convenience */
|
|
11
|
+
props?: T;
|
|
12
|
+
settings?: BlockSettings & Record<string, any>;
|
|
13
|
+
}
|
|
14
|
+
export interface HeroBlockData {
|
|
15
|
+
title: string;
|
|
16
|
+
subtitle?: string;
|
|
17
|
+
image?: string;
|
|
18
|
+
overlay?: boolean;
|
|
19
|
+
buttons?: Array<{
|
|
20
|
+
text: string;
|
|
21
|
+
url: string;
|
|
22
|
+
variant?: 'primary' | 'outline' | 'white';
|
|
23
|
+
}>;
|
|
24
|
+
stats?: Array<{
|
|
25
|
+
value: string;
|
|
26
|
+
label: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
export interface HtmlBlockData {
|
|
30
|
+
content: string;
|
|
31
|
+
}
|
|
32
|
+
export interface CardsBlockData {
|
|
33
|
+
title?: string;
|
|
34
|
+
subtitle?: string;
|
|
35
|
+
cards: Array<{
|
|
36
|
+
title: string;
|
|
37
|
+
desc: string;
|
|
38
|
+
icon?: string;
|
|
39
|
+
image?: string;
|
|
40
|
+
link?: string;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
export interface ImageTextBlockData {
|
|
44
|
+
title?: string;
|
|
45
|
+
content?: string;
|
|
46
|
+
image?: string;
|
|
47
|
+
items?: Array<{
|
|
48
|
+
text: string;
|
|
49
|
+
icon?: string;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
export interface PricingPlan {
|
|
53
|
+
id?: string;
|
|
54
|
+
name: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
price: string | number;
|
|
57
|
+
period?: string;
|
|
58
|
+
features?: string[];
|
|
59
|
+
actionUrl?: string;
|
|
60
|
+
actionText?: string;
|
|
61
|
+
tariffGroupName?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface PricingBlockData {
|
|
64
|
+
title?: string;
|
|
65
|
+
subtitle?: string;
|
|
66
|
+
plans?: PricingPlan[];
|
|
67
|
+
}
|
|
68
|
+
export interface Office {
|
|
69
|
+
name: string;
|
|
70
|
+
address?: string;
|
|
71
|
+
phone?: string;
|
|
72
|
+
email?: string;
|
|
73
|
+
hours?: string;
|
|
74
|
+
isMain?: boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface ContactsBlockData {
|
|
77
|
+
title?: string;
|
|
78
|
+
subtitle?: string;
|
|
79
|
+
showBankDetails?: boolean;
|
|
80
|
+
showForm?: boolean;
|
|
81
|
+
offices?: Office[];
|
|
82
|
+
}
|
|
83
|
+
export interface GalleryBlockData {
|
|
84
|
+
title?: string;
|
|
85
|
+
images: Array<{
|
|
86
|
+
url: string;
|
|
87
|
+
alt?: string;
|
|
88
|
+
caption?: string;
|
|
89
|
+
}>;
|
|
90
|
+
}
|
|
91
|
+
export interface MapBlockData {
|
|
92
|
+
title?: string;
|
|
93
|
+
center: {
|
|
94
|
+
lat: number;
|
|
95
|
+
lng: number;
|
|
96
|
+
};
|
|
97
|
+
zoom?: number;
|
|
98
|
+
markers?: Array<{
|
|
99
|
+
lat: number;
|
|
100
|
+
lng: number;
|
|
101
|
+
label?: string;
|
|
102
|
+
}>;
|
|
103
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue';
|
|
2
|
+
import type { Block } from './blocks.js';
|
|
3
|
+
export interface PageContext {
|
|
4
|
+
title?: string;
|
|
5
|
+
type?: string;
|
|
6
|
+
author?: string;
|
|
7
|
+
featuredImage?: string;
|
|
8
|
+
excerpt?: string;
|
|
9
|
+
isGated?: boolean;
|
|
10
|
+
termIds?: string[];
|
|
11
|
+
layout?: string;
|
|
12
|
+
publishedAt?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const PAGE_CONTEXT_KEY: InjectionKey<Ref<PageContext>>;
|
|
15
|
+
export interface SiteConfig {
|
|
16
|
+
template: string;
|
|
17
|
+
version: string;
|
|
18
|
+
skin: string;
|
|
19
|
+
autoUpdate?: boolean;
|
|
20
|
+
branding: {
|
|
21
|
+
logo?: string;
|
|
22
|
+
favicon?: string;
|
|
23
|
+
siteName?: string;
|
|
24
|
+
};
|
|
25
|
+
navigation: NavItem[];
|
|
26
|
+
locales: string[];
|
|
27
|
+
defaultLocale: string;
|
|
28
|
+
footer?: {
|
|
29
|
+
copyright?: string;
|
|
30
|
+
links?: Array<{
|
|
31
|
+
text: string;
|
|
32
|
+
url: string;
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
features?: Record<string, boolean | Record<string, any>>;
|
|
36
|
+
}
|
|
37
|
+
export interface NavItem {
|
|
38
|
+
text: string | Record<string, string>;
|
|
39
|
+
url: string;
|
|
40
|
+
external?: boolean;
|
|
41
|
+
children?: NavItem[];
|
|
42
|
+
}
|
|
43
|
+
export interface PageConfig {
|
|
44
|
+
title: string;
|
|
45
|
+
blocks: Block[];
|
|
46
|
+
meta?: {
|
|
47
|
+
description?: string;
|
|
48
|
+
ogTitle?: string;
|
|
49
|
+
ogImage?: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface SiteData {
|
|
53
|
+
config: SiteConfig;
|
|
54
|
+
locale: string;
|
|
55
|
+
[key: string]: any;
|
|
56
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface Skin {
|
|
2
|
+
name: string;
|
|
3
|
+
colors: {
|
|
4
|
+
primary: string;
|
|
5
|
+
primaryDark?: string;
|
|
6
|
+
primaryLight?: string;
|
|
7
|
+
secondary?: string;
|
|
8
|
+
accent?: string;
|
|
9
|
+
background: string;
|
|
10
|
+
backgroundAlt?: string;
|
|
11
|
+
surface: string;
|
|
12
|
+
text: string;
|
|
13
|
+
textLight?: string;
|
|
14
|
+
border?: string;
|
|
15
|
+
success?: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
};
|
|
18
|
+
typography: {
|
|
19
|
+
fontFamily: string;
|
|
20
|
+
headingFont?: string;
|
|
21
|
+
baseFontSize?: string;
|
|
22
|
+
};
|
|
23
|
+
shape: {
|
|
24
|
+
borderRadius: string;
|
|
25
|
+
cardElevation?: number;
|
|
26
|
+
maxWidth?: string;
|
|
27
|
+
};
|
|
28
|
+
components?: {
|
|
29
|
+
navbar?: {
|
|
30
|
+
variant?: 'fixed' | 'static';
|
|
31
|
+
transparent?: boolean;
|
|
32
|
+
};
|
|
33
|
+
footer?: {
|
|
34
|
+
variant?: 'dark' | 'light';
|
|
35
|
+
};
|
|
36
|
+
hero?: {
|
|
37
|
+
variant?: 'fullscreen' | 'compact';
|
|
38
|
+
overlay?: boolean;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WorkerEnv, WorkerConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a Cloudflare Worker handler for an Xosen site template.
|
|
4
|
+
*
|
|
5
|
+
* The worker:
|
|
6
|
+
* - Serves static assets from ASSETS binding
|
|
7
|
+
* - Provides /api/content/:key endpoint for KV reads
|
|
8
|
+
* - Injects preloaded KV data as window.__SITE_DATA__ into HTML pages
|
|
9
|
+
* - Detects locale from query param or Accept-Language header
|
|
10
|
+
* - Preloads page-specific data for /p/:slug routes
|
|
11
|
+
*/
|
|
12
|
+
export declare function createSiteWorker(config: WorkerConfig): {
|
|
13
|
+
fetch: (request: Request, env: WorkerEnv) => Promise<Response>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal CF Workers type declarations to avoid requiring @cloudflare/workers-types.
|
|
3
|
+
* These match the Cloudflare Workers runtime API.
|
|
4
|
+
*/
|
|
5
|
+
export interface CfKVNamespace {
|
|
6
|
+
get(key: string, options?: any): Promise<string | null>;
|
|
7
|
+
put(key: string, value: string | ArrayBuffer | ReadableStream, options?: any): Promise<void>;
|
|
8
|
+
delete(key: string): Promise<void>;
|
|
9
|
+
list(options?: any): Promise<any>;
|
|
10
|
+
}
|
|
11
|
+
export interface CfFetcher {
|
|
12
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
13
|
+
}
|
|
14
|
+
export interface WorkerEnv {
|
|
15
|
+
SITE_CONTENT: CfKVNamespace;
|
|
16
|
+
ASSETS: CfFetcher;
|
|
17
|
+
}
|
|
18
|
+
export interface WorkerConfig {
|
|
19
|
+
/** Default locale for the site */
|
|
20
|
+
defaultLocale: string;
|
|
21
|
+
/** Supported locales for Accept-Language detection */
|
|
22
|
+
supportedLocales?: string[];
|
|
23
|
+
/** Site name for meta title suffix, e.g. 'SWI' */
|
|
24
|
+
siteName?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface PageData {
|
|
27
|
+
title?: string;
|
|
28
|
+
blocks?: unknown[];
|
|
29
|
+
html?: string;
|
|
30
|
+
meta?: {
|
|
31
|
+
description?: string;
|
|
32
|
+
ogTitle?: string;
|
|
33
|
+
ogImage?: string;
|
|
34
|
+
};
|
|
35
|
+
type?: string;
|
|
36
|
+
author?: string;
|
|
37
|
+
featuredImage?: string;
|
|
38
|
+
excerpt?: string;
|
|
39
|
+
termIds?: string[];
|
|
40
|
+
layout?: string;
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xosen/site-sdk",
|
|
3
|
-
"version": "0.0.
|
|
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",
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"src"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"build": "vue-tsc --declaration --emitDeclarationOnly --outDir dist
|
|
28
|
+
"build": "vite build && vue-tsc --declaration --emitDeclarationOnly --outDir dist",
|
|
29
29
|
"dev": "vite build --watch",
|
|
30
30
|
"clean": "rm -rf dist"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"vue": "
|
|
34
|
-
"vue-i18n": "
|
|
35
|
-
"vue-router": "
|
|
33
|
+
"vue": "^3.5.0",
|
|
34
|
+
"vue-i18n": "^11.0.0",
|
|
35
|
+
"vue-router": "^5.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"vue": "catalog:",
|
|
@@ -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
|