@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xosen/site-sdk",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Shared Vue components and composables for Xosen site templates",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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.text }}
12
+ {{ navText(item) }}
13
13
  </a>
14
14
  <router-link v-else :to="item.url" class="x-nav__link" @click="menuOpen = false">
15
- {{ item.text }}
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 { navigation, branding, locales } = useSiteConfig();
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(false);
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
@@ -26,7 +26,7 @@ export interface SiteConfig {
26
26
  }
27
27
 
28
28
  export interface NavItem {
29
- text: string;
29
+ text: string | Record<string, string>;
30
30
  url: string;
31
31
  external?: boolean;
32
32
  children?: NavItem[];
@@ -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
- const value = await env.SITE_CONTENT.get(key);
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
- const content: Record<string, PageData> = contentRaw ? JSON.parse(contentRaw) : {};
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
- const value = await env.SITE_CONTENT.get(`page:${slug}:${locale}`);
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
  }