@xosen/site-sdk 0.0.4 → 0.0.6
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/index.js +369 -342
- package/package.json +1 -1
- package/src/components/layout/XSiteNav.vue +11 -3
- package/src/composables/useLivePreview.ts +25 -11
- package/src/composables/useSiteConfig.ts +2 -0
- package/src/index.ts +1 -1
- package/src/types/config.ts +1 -1
- package/src/worker/create-site-worker.ts +26 -5
package/package.json
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
<div class="x-nav__links" :class="{ 'x-nav__links--open': menuOpen }">
|
|
10
10
|
<template v-for="item in navigation" :key="item.url">
|
|
11
11
|
<a v-if="item.external" :href="item.url" class="x-nav__link" target="_blank" @click="menuOpen = false">
|
|
12
|
-
{{ item
|
|
12
|
+
{{ navText(item) }}
|
|
13
13
|
</a>
|
|
14
14
|
<router-link v-else :to="item.url" class="x-nav__link" @click="menuOpen = false">
|
|
15
|
-
{{ item
|
|
15
|
+
{{ navText(item) }}
|
|
16
16
|
</router-link>
|
|
17
17
|
</template>
|
|
18
18
|
|
|
@@ -30,11 +30,19 @@
|
|
|
30
30
|
|
|
31
31
|
<script setup lang="ts">
|
|
32
32
|
import { ref, onMounted, onUnmounted } from 'vue';
|
|
33
|
+
import { useI18n } from 'vue-i18n';
|
|
33
34
|
|
|
35
|
+
import type { NavItem } from '../../types/config.js';
|
|
34
36
|
import { useSiteConfig } from '../../composables/useSiteConfig.js';
|
|
35
37
|
import XLocaleSwitcher from '../common/XLocaleSwitcher.vue';
|
|
36
38
|
|
|
37
|
-
const {
|
|
39
|
+
const { locale } = useI18n();
|
|
40
|
+
const { navigation, branding, locales, defaultLocale } = useSiteConfig();
|
|
41
|
+
|
|
42
|
+
function navText(item: NavItem): string {
|
|
43
|
+
if (typeof item.text === 'string') return item.text;
|
|
44
|
+
return item.text[locale.value] || item.text[defaultLocale.value] || Object.values(item.text)[0] || '';
|
|
45
|
+
}
|
|
38
46
|
|
|
39
47
|
const scrolled = ref(false);
|
|
40
48
|
const menuOpen = ref(false);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ref, onMounted, onUnmounted } from 'vue';
|
|
2
|
+
import type { Router } from 'vue-router';
|
|
2
3
|
|
|
3
4
|
export interface PreviewMessage {
|
|
4
5
|
type: 'xosen-preview-update';
|
|
@@ -11,20 +12,37 @@ export interface PreviewMessage {
|
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/** Check if running inside an iframe (preview mode) */
|
|
16
|
+
export function isInPreview(): boolean {
|
|
17
|
+
try {
|
|
18
|
+
return window.self !== window.top;
|
|
19
|
+
} catch {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Install a router guard that blocks all navigation in preview mode.
|
|
26
|
+
* Call this once during app setup (e.g. in App.vue or router config).
|
|
27
|
+
*/
|
|
28
|
+
export function setupPreviewRouter(router: Router) {
|
|
29
|
+
if (!isInPreview()) return;
|
|
30
|
+
|
|
31
|
+
router.beforeEach((_to, from) => {
|
|
32
|
+
// Allow initial navigation, block subsequent ones
|
|
33
|
+
if (!from.name) return true;
|
|
34
|
+
return false;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
14
38
|
/**
|
|
15
39
|
* Listen for live preview messages from the admin UI.
|
|
16
40
|
* When the admin sends page data via postMessage, this composable
|
|
17
41
|
* provides reactive access to the preview data.
|
|
18
|
-
*
|
|
19
|
-
* Usage in template ContentPage:
|
|
20
|
-
* ```ts
|
|
21
|
-
* const { previewPage } = useLivePreview();
|
|
22
|
-
* // If previewPage matches current slug+locale, use it instead of KV/API data
|
|
23
|
-
* ```
|
|
24
42
|
*/
|
|
25
43
|
export function useLivePreview() {
|
|
26
44
|
const previewPage = ref<PreviewMessage['page'] | null>(null);
|
|
27
|
-
const isPreviewMode = ref(
|
|
45
|
+
const isPreviewMode = ref(isInPreview());
|
|
28
46
|
|
|
29
47
|
function handleMessage(event: MessageEvent) {
|
|
30
48
|
const data = event.data;
|
|
@@ -36,10 +54,6 @@ export function useLivePreview() {
|
|
|
36
54
|
|
|
37
55
|
onMounted(() => {
|
|
38
56
|
window.addEventListener('message', handleMessage);
|
|
39
|
-
// Notify parent that preview is ready
|
|
40
|
-
if (window.parent !== window) {
|
|
41
|
-
window.parent.postMessage({ type: 'xosen-preview-ready' }, '*');
|
|
42
|
-
}
|
|
43
57
|
});
|
|
44
58
|
|
|
45
59
|
onUnmounted(() => {
|
|
@@ -9,6 +9,7 @@ export function useSiteConfig() {
|
|
|
9
9
|
const siteData = (window as any).__SITE_DATA__ as SiteData | undefined;
|
|
10
10
|
|
|
11
11
|
const config = computed<SiteConfig | null>(() => siteData?.config || null);
|
|
12
|
+
const components = computed<Record<string, any>>(() => (siteData as any)?.components || {});
|
|
12
13
|
|
|
13
14
|
const navigation = computed(() => config.value?.navigation || []);
|
|
14
15
|
const locales = computed(() => config.value?.locales || []);
|
|
@@ -26,6 +27,7 @@ export function useSiteConfig() {
|
|
|
26
27
|
|
|
27
28
|
return {
|
|
28
29
|
config,
|
|
30
|
+
components,
|
|
29
31
|
navigation,
|
|
30
32
|
locales,
|
|
31
33
|
defaultLocale,
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ export { useSiteData } from './composables/useSiteData.js';
|
|
|
22
22
|
export { useSiteConfig } from './composables/useSiteConfig.js';
|
|
23
23
|
export { useSkin } from './composables/useSkin.js';
|
|
24
24
|
export { useSiteApi } from './composables/useSiteApi.js';
|
|
25
|
-
export { useLivePreview } from './composables/useLivePreview.js';
|
|
25
|
+
export { useLivePreview, setupPreviewRouter, isInPreview } from './composables/useLivePreview.js';
|
|
26
26
|
export type { PreviewMessage } from './composables/useLivePreview.js';
|
|
27
27
|
|
|
28
28
|
// Worker
|
package/src/types/config.ts
CHANGED
|
@@ -17,7 +17,7 @@ function detectLocale(request: Request, config: WorkerConfig): string {
|
|
|
17
17
|
return config.defaultLocale;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function handleContentApi(url: URL, env: WorkerEnv): Promise<Response> | null {
|
|
20
|
+
function handleContentApi(url: URL, env: WorkerEnv, defaultLocale: string): Promise<Response> | null {
|
|
21
21
|
if (!url.pathname.startsWith('/api/content/')) return null;
|
|
22
22
|
|
|
23
23
|
const key = decodeURIComponent(url.pathname.replace('/api/content/', ''));
|
|
@@ -26,7 +26,16 @@ function handleContentApi(url: URL, env: WorkerEnv): Promise<Response> | null {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
return (async () => {
|
|
29
|
-
|
|
29
|
+
let value = await env.SITE_CONTENT.get(key);
|
|
30
|
+
|
|
31
|
+
// Fallback to default locale
|
|
32
|
+
if (!value) {
|
|
33
|
+
const lastColon = key.lastIndexOf(':');
|
|
34
|
+
if (lastColon > 0 && key.slice(lastColon + 1) !== defaultLocale) {
|
|
35
|
+
value = await env.SITE_CONTENT.get(key.slice(0, lastColon + 1) + defaultLocale);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
if (!value) {
|
|
31
40
|
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
32
41
|
}
|
|
@@ -81,7 +90,7 @@ export function createSiteWorker(config: WorkerConfig): {
|
|
|
81
90
|
const url = new URL(request.url);
|
|
82
91
|
|
|
83
92
|
// API route: /api/content/:key
|
|
84
|
-
const apiResponse = handleContentApi(url, env);
|
|
93
|
+
const apiResponse = handleContentApi(url, env, config.defaultLocale);
|
|
85
94
|
if (apiResponse) return apiResponse;
|
|
86
95
|
|
|
87
96
|
// Static assets
|
|
@@ -95,6 +104,8 @@ export function createSiteWorker(config: WorkerConfig): {
|
|
|
95
104
|
const assetResponse = await env.ASSETS.fetch(new Request(indexUrl));
|
|
96
105
|
let html = await assetResponse.text();
|
|
97
106
|
|
|
107
|
+
const fallbackLocale = config.defaultLocale;
|
|
108
|
+
|
|
98
109
|
// Read 2 KV keys: config + content for locale
|
|
99
110
|
const [configRaw, contentRaw] = await Promise.all([
|
|
100
111
|
env.SITE_CONTENT.get('config'),
|
|
@@ -102,7 +113,13 @@ export function createSiteWorker(config: WorkerConfig): {
|
|
|
102
113
|
]);
|
|
103
114
|
|
|
104
115
|
const siteConfig = configRaw ? JSON.parse(configRaw) : {};
|
|
105
|
-
|
|
116
|
+
let content: Record<string, PageData> = contentRaw ? JSON.parse(contentRaw) : {};
|
|
117
|
+
|
|
118
|
+
// Fallback to default locale if no content for requested locale
|
|
119
|
+
if (!contentRaw && locale !== fallbackLocale) {
|
|
120
|
+
const fallbackRaw = await env.SITE_CONTENT.get(`content:${fallbackLocale}`);
|
|
121
|
+
if (fallbackRaw) content = JSON.parse(fallbackRaw);
|
|
122
|
+
}
|
|
106
123
|
|
|
107
124
|
const pageMatch = url.pathname.match(/^\/p\/(.+)$/);
|
|
108
125
|
|
|
@@ -110,7 +127,11 @@ export function createSiteWorker(config: WorkerConfig): {
|
|
|
110
127
|
if (pageMatch) {
|
|
111
128
|
const slug = pageMatch[1];
|
|
112
129
|
if (!content[slug]) {
|
|
113
|
-
|
|
130
|
+
let value = await env.SITE_CONTENT.get(`page:${slug}:${locale}`);
|
|
131
|
+
// Fallback to default locale
|
|
132
|
+
if (!value && locale !== fallbackLocale) {
|
|
133
|
+
value = await env.SITE_CONTENT.get(`page:${slug}:${fallbackLocale}`);
|
|
134
|
+
}
|
|
114
135
|
if (value) content[slug] = JSON.parse(value);
|
|
115
136
|
}
|
|
116
137
|
}
|