@vigilkids/cms-nuxt 0.0.1
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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +45 -0
- package/dist/module.d.ts +45 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +78 -0
- package/dist/runtime/composables/useArticle.d.ts +0 -0
- package/dist/runtime/composables/useArticle.js +62 -0
- package/dist/runtime/composables/useArticleList.d.ts +0 -0
- package/dist/runtime/composables/useArticleList.js +15 -0
- package/dist/runtime/composables/useAuthor.d.ts +0 -0
- package/dist/runtime/composables/useAuthor.js +23 -0
- package/dist/runtime/composables/useBlogSeo.d.ts +0 -0
- package/dist/runtime/composables/useBlogSeo.js +16 -0
- package/dist/runtime/composables/useCategories.d.ts +0 -0
- package/dist/runtime/composables/useCategories.js +27 -0
- package/dist/runtime/composables/useCmsClient.d.ts +0 -0
- package/dist/runtime/composables/useCmsClient.js +13 -0
- package/dist/runtime/composables/useCmsPreview.d.ts +0 -0
- package/dist/runtime/composables/useCmsPreview.js +16 -0
- package/dist/runtime/plugins/preview-init.client.d.ts +0 -0
- package/dist/runtime/plugins/preview-init.client.js +10 -0
- package/dist/runtime/server/api/_revalidate.post.d.ts +0 -0
- package/dist/runtime/server/api/_revalidate.post.js +41 -0
- package/dist/runtime/server/api/preview.get.d.ts +0 -0
- package/dist/runtime/server/api/preview.get.js +31 -0
- package/dist/runtime/server/middleware/redirects.d.ts +0 -0
- package/dist/runtime/server/middleware/redirects.js +38 -0
- package/dist/runtime/server/utils/cms-client.d.ts +0 -0
- package/dist/runtime/server/utils/cms-client.js +15 -0
- package/dist/runtime/sitemap/provider.d.ts +0 -0
- package/dist/runtime/sitemap/provider.js +26 -0
- package/dist/types.d.mts +7 -0
- package/dist/types.d.ts +7 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present VigilKids
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @vigilkids/cms-nuxt
|
|
2
|
+
|
|
3
|
+
Nuxt 3 module for OneX CMS integration. Provides auto-imported composables, preview mode, and sitemap integration.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @vigilkids/cms-nuxt
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Peer Dependencies:**
|
|
12
|
+
- `nuxt` ≥ 3.15
|
|
13
|
+
- `@vigilkids/cms-client` (workspace)
|
|
14
|
+
- `@nuxtjs/sitemap` (optional)
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// nuxt.config.ts
|
|
20
|
+
export default defineNuxtConfig({
|
|
21
|
+
modules: ['@vigilkids/cms-nuxt'],
|
|
22
|
+
runtimeConfig: {
|
|
23
|
+
cmsApiBaseUrl: 'https://api.example.com',
|
|
24
|
+
cmsApiKey: '',
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Auto-Imported Composables
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// Available in any Vue component without import
|
|
33
|
+
const { article, pending } = useCmsArticle('my-slug', { locale: 'en' })
|
|
34
|
+
const { isPreview, previewToken } = useCmsPreview()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Preview Mode
|
|
38
|
+
|
|
39
|
+
The module registers a server route at `/api/preview` that validates preview tokens and sets a secure httpOnly cookie. Frontend middleware automatically detects preview mode.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
GET /api/preview?token=xxx&slug=my-article&locale=en
|
|
43
|
+
→ Sets preview cookie → Redirects to article page
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Sitemap
|
|
47
|
+
|
|
48
|
+
When `@nuxtjs/sitemap` is installed, the module automatically provides CMS article URLs to the sitemap.
|
|
49
|
+
|
|
50
|
+
## Compatibility
|
|
51
|
+
|
|
52
|
+
- Nuxt ≥ 3.15
|
|
53
|
+
- Node.js ≥ 18
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
[MIT](./LICENSE)
|
package/dist/module.cjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NuxtModule } from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
/** CMS Nuxt Module 配置 */
|
|
4
|
+
interface CmsNuxtModuleOptions {
|
|
5
|
+
/** CMS API 基础 URL */
|
|
6
|
+
apiUrl: string;
|
|
7
|
+
/** 产品编码 */
|
|
8
|
+
productCode: string;
|
|
9
|
+
/** 默认语言 */
|
|
10
|
+
defaultLocale?: string;
|
|
11
|
+
/** Webhook HMAC-SHA256 密钥(服务端专用) */
|
|
12
|
+
webhookSecret: string;
|
|
13
|
+
/** 预览模式配置 */
|
|
14
|
+
preview?: {
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
cookieName?: string;
|
|
17
|
+
cookieMaxAge?: number;
|
|
18
|
+
/** 文章路径模式,slug 会替换 {slug} 占位符。默认 '/blog/{slug}' */
|
|
19
|
+
articlePath?: string;
|
|
20
|
+
};
|
|
21
|
+
/** ISR 重验证配置 */
|
|
22
|
+
revalidate?: {
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
path?: string;
|
|
25
|
+
};
|
|
26
|
+
/** 重定向中间件配置 */
|
|
27
|
+
redirects?: {
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
cacheTtl?: number;
|
|
30
|
+
};
|
|
31
|
+
/** Sitemap 集成 */
|
|
32
|
+
sitemap?: {
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
locales?: string[];
|
|
35
|
+
};
|
|
36
|
+
/** 语言代码映射 (前端 locale → CMS API locale) */
|
|
37
|
+
localeMap?: Record<string, string>;
|
|
38
|
+
/** URL 路径前缀映射 (CMS locale → URL prefix),默认语言无需配置 */
|
|
39
|
+
localePrefixes?: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare const module: NuxtModule<CmsNuxtModuleOptions>;
|
|
43
|
+
|
|
44
|
+
export { module as default };
|
|
45
|
+
export type { CmsNuxtModuleOptions };
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NuxtModule } from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
/** CMS Nuxt Module 配置 */
|
|
4
|
+
interface CmsNuxtModuleOptions {
|
|
5
|
+
/** CMS API 基础 URL */
|
|
6
|
+
apiUrl: string;
|
|
7
|
+
/** 产品编码 */
|
|
8
|
+
productCode: string;
|
|
9
|
+
/** 默认语言 */
|
|
10
|
+
defaultLocale?: string;
|
|
11
|
+
/** Webhook HMAC-SHA256 密钥(服务端专用) */
|
|
12
|
+
webhookSecret: string;
|
|
13
|
+
/** 预览模式配置 */
|
|
14
|
+
preview?: {
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
cookieName?: string;
|
|
17
|
+
cookieMaxAge?: number;
|
|
18
|
+
/** 文章路径模式,slug 会替换 {slug} 占位符。默认 '/blog/{slug}' */
|
|
19
|
+
articlePath?: string;
|
|
20
|
+
};
|
|
21
|
+
/** ISR 重验证配置 */
|
|
22
|
+
revalidate?: {
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
path?: string;
|
|
25
|
+
};
|
|
26
|
+
/** 重定向中间件配置 */
|
|
27
|
+
redirects?: {
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
cacheTtl?: number;
|
|
30
|
+
};
|
|
31
|
+
/** Sitemap 集成 */
|
|
32
|
+
sitemap?: {
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
locales?: string[];
|
|
35
|
+
};
|
|
36
|
+
/** 语言代码映射 (前端 locale → CMS API locale) */
|
|
37
|
+
localeMap?: Record<string, string>;
|
|
38
|
+
/** URL 路径前缀映射 (CMS locale → URL prefix),默认语言无需配置 */
|
|
39
|
+
localePrefixes?: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare const module: NuxtModule<CmsNuxtModuleOptions>;
|
|
43
|
+
|
|
44
|
+
export { module as default };
|
|
45
|
+
export type { CmsNuxtModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addImportsDir, addServerImportsDir, addServerHandler, addPlugin } from '@nuxt/kit';
|
|
2
|
+
|
|
3
|
+
const module = defineNuxtModule({
|
|
4
|
+
meta: {
|
|
5
|
+
name: "@vigilkids/cms-nuxt",
|
|
6
|
+
configKey: "cms",
|
|
7
|
+
compatibility: { nuxt: ">=3.15.0" }
|
|
8
|
+
},
|
|
9
|
+
defaults: {
|
|
10
|
+
apiUrl: "",
|
|
11
|
+
productCode: "",
|
|
12
|
+
defaultLocale: "en",
|
|
13
|
+
webhookSecret: "",
|
|
14
|
+
preview: { cookieName: "__preview_token", cookieMaxAge: 1800 },
|
|
15
|
+
revalidate: { enabled: true, path: "/api/_revalidate" },
|
|
16
|
+
redirects: { enabled: true, cacheTtl: 3e5 },
|
|
17
|
+
sitemap: { enabled: false },
|
|
18
|
+
localeMap: {},
|
|
19
|
+
localePrefixes: {}
|
|
20
|
+
},
|
|
21
|
+
setup(options, nuxt) {
|
|
22
|
+
const { resolve } = createResolver(import.meta.url);
|
|
23
|
+
nuxt.options.runtimeConfig.cms = {
|
|
24
|
+
webhookSecret: options.webhookSecret,
|
|
25
|
+
redirects: {
|
|
26
|
+
enabled: options.redirects?.enabled ?? true,
|
|
27
|
+
cacheTtl: options.redirects?.cacheTtl ?? 3e5
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
nuxt.options.runtimeConfig.public.cms = {
|
|
31
|
+
apiUrl: options.apiUrl,
|
|
32
|
+
productCode: options.productCode,
|
|
33
|
+
defaultLocale: options.defaultLocale ?? "en",
|
|
34
|
+
preview: {
|
|
35
|
+
cookieName: options.preview?.cookieName ?? "__preview_token",
|
|
36
|
+
cookieMaxAge: options.preview?.cookieMaxAge ?? 1800,
|
|
37
|
+
articlePath: options.preview?.articlePath ?? "/blog/{slug}"
|
|
38
|
+
},
|
|
39
|
+
localeMap: options.localeMap ?? {},
|
|
40
|
+
localePrefixes: options.localePrefixes ?? {}
|
|
41
|
+
};
|
|
42
|
+
addImportsDir(resolve("./runtime/composables"));
|
|
43
|
+
addServerImportsDir(resolve("./runtime/server/utils"));
|
|
44
|
+
if (options.revalidate?.enabled !== false) {
|
|
45
|
+
addServerHandler({
|
|
46
|
+
route: options.revalidate?.path ?? "/api/_revalidate",
|
|
47
|
+
method: "post",
|
|
48
|
+
handler: resolve("./runtime/server/api/_revalidate.post")
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (options.preview?.enabled !== false) {
|
|
52
|
+
addServerHandler({
|
|
53
|
+
route: "/api/preview",
|
|
54
|
+
method: "get",
|
|
55
|
+
handler: resolve("./runtime/server/api/preview.get")
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (options.redirects?.enabled !== false) {
|
|
59
|
+
addServerHandler({
|
|
60
|
+
middleware: true,
|
|
61
|
+
handler: resolve("./runtime/server/middleware/redirects")
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
addPlugin({
|
|
65
|
+
src: resolve("./runtime/plugins/preview-init.client"),
|
|
66
|
+
mode: "client"
|
|
67
|
+
});
|
|
68
|
+
if (options.sitemap?.enabled) {
|
|
69
|
+
nuxt.hook("sitemap:resolve", async (ctx) => {
|
|
70
|
+
const { sitemapProvider } = await import('../dist/runtime/sitemap/provider.js');
|
|
71
|
+
const entries = await sitemapProvider(options);
|
|
72
|
+
ctx.urls.push(...entries);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export { module as default };
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { toRef } from "vue";
|
|
2
|
+
import { useAsyncData, useHead, useRuntimeConfig, useSeoMeta } from "#imports";
|
|
3
|
+
import { useCmsClient } from "./useCmsClient.js";
|
|
4
|
+
export function useArticle(slug) {
|
|
5
|
+
const resolvedSlug = toRef(slug);
|
|
6
|
+
const client = useCmsClient();
|
|
7
|
+
const _articleAsync = useAsyncData(
|
|
8
|
+
`cms-article-${resolvedSlug.value}`,
|
|
9
|
+
() => client.getArticle(resolvedSlug.value),
|
|
10
|
+
{ watch: [resolvedSlug] }
|
|
11
|
+
);
|
|
12
|
+
const { data: article, status, error } = _articleAsync;
|
|
13
|
+
const { data: relatedArticles } = useAsyncData(
|
|
14
|
+
`cms-related-${resolvedSlug.value}`,
|
|
15
|
+
() => client.getRelatedArticles(resolvedSlug.value),
|
|
16
|
+
{ lazy: true, watch: [resolvedSlug] }
|
|
17
|
+
);
|
|
18
|
+
useSeoMeta({
|
|
19
|
+
title: () => article.value?.seo_meta?.title ?? article.value?.title ?? "",
|
|
20
|
+
description: () => article.value?.seo_meta?.description ?? article.value?.excerpt ?? "",
|
|
21
|
+
ogTitle: () => article.value?.og_meta?.title ?? article.value?.title ?? "",
|
|
22
|
+
ogDescription: () => article.value?.og_meta?.description ?? article.value?.excerpt ?? "",
|
|
23
|
+
ogImage: () => article.value?.og_meta?.image ?? article.value?.featured_image_url ?? void 0,
|
|
24
|
+
ogType: "article",
|
|
25
|
+
twitterCard: () => article.value?.twitter_meta?.card ?? "summary_large_image",
|
|
26
|
+
twitterTitle: () => article.value?.twitter_meta?.title ?? article.value?.title ?? "",
|
|
27
|
+
twitterDescription: () => article.value?.twitter_meta?.description ?? article.value?.excerpt ?? "",
|
|
28
|
+
twitterImage: () => article.value?.twitter_meta?.image ?? article.value?.featured_image_url ?? void 0
|
|
29
|
+
});
|
|
30
|
+
useHead({
|
|
31
|
+
script: () => {
|
|
32
|
+
if (!article.value?.json_ld || Object.keys(article.value.json_ld).length === 0) return [];
|
|
33
|
+
return [{ type: "application/ld+json", innerHTML: JSON.stringify(article.value.json_ld) }];
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
useHead({
|
|
37
|
+
link: () => {
|
|
38
|
+
const a = article.value;
|
|
39
|
+
if (!a?.available_locales || a.available_locales.length <= 1) return [];
|
|
40
|
+
const publicConfig = useRuntimeConfig().public;
|
|
41
|
+
const siteUrl = publicConfig.siteUrl ?? "";
|
|
42
|
+
const links = a.available_locales.map((locale) => ({
|
|
43
|
+
rel: "alternate",
|
|
44
|
+
hreflang: locale,
|
|
45
|
+
href: locale === "en" ? `${siteUrl}/blog/${a.slug}` : `${siteUrl}/${locale}/blog/${a.slug}`
|
|
46
|
+
}));
|
|
47
|
+
links.push({
|
|
48
|
+
rel: "alternate",
|
|
49
|
+
hreflang: "x-default",
|
|
50
|
+
href: `${siteUrl}/blog/${a.slug}`
|
|
51
|
+
});
|
|
52
|
+
return links;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
article,
|
|
57
|
+
relatedArticles,
|
|
58
|
+
status,
|
|
59
|
+
error,
|
|
60
|
+
ready: _articleAsync
|
|
61
|
+
};
|
|
62
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { computed, toRef } from "vue";
|
|
2
|
+
import { useAsyncData } from "#imports";
|
|
3
|
+
import { useCmsClient } from "./useCmsClient.js";
|
|
4
|
+
export function useArticleList(params) {
|
|
5
|
+
const resolvedParams = toRef(params);
|
|
6
|
+
const client = useCmsClient();
|
|
7
|
+
const { data, status, error, refresh } = useAsyncData(
|
|
8
|
+
`cms-articles-${JSON.stringify(resolvedParams.value)}`,
|
|
9
|
+
() => client.getArticles(resolvedParams.value),
|
|
10
|
+
{ watch: [resolvedParams] }
|
|
11
|
+
);
|
|
12
|
+
const articles = computed(() => data.value?.data ?? []);
|
|
13
|
+
const pagination = computed(() => data.value?.pagination ?? null);
|
|
14
|
+
return { articles, pagination, status, error, refresh };
|
|
15
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { toRef } from "vue";
|
|
2
|
+
import { useAsyncData } from "#imports";
|
|
3
|
+
import { useCmsClient } from "./useCmsClient.js";
|
|
4
|
+
export function useAuthor(slug) {
|
|
5
|
+
const resolvedSlug = toRef(slug);
|
|
6
|
+
const client = useCmsClient();
|
|
7
|
+
const { data: author, status, error } = useAsyncData(
|
|
8
|
+
`cms-author-${resolvedSlug.value}`,
|
|
9
|
+
() => client.getAuthor(resolvedSlug.value),
|
|
10
|
+
{ watch: [resolvedSlug] }
|
|
11
|
+
);
|
|
12
|
+
const { data: articles } = useAsyncData(
|
|
13
|
+
`cms-author-articles-${resolvedSlug.value}`,
|
|
14
|
+
() => client.getAuthorArticles(resolvedSlug.value),
|
|
15
|
+
{ lazy: true, watch: [resolvedSlug] }
|
|
16
|
+
);
|
|
17
|
+
return {
|
|
18
|
+
author,
|
|
19
|
+
articles,
|
|
20
|
+
status,
|
|
21
|
+
error
|
|
22
|
+
};
|
|
23
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { toRef } from "vue";
|
|
2
|
+
import { useSeoMeta } from "#imports";
|
|
3
|
+
export function useBlogSeo(options) {
|
|
4
|
+
const title = toRef(options.title);
|
|
5
|
+
const description = toRef(options.description ?? "");
|
|
6
|
+
const image = toRef(options.image);
|
|
7
|
+
useSeoMeta({
|
|
8
|
+
title: () => title.value,
|
|
9
|
+
description: () => description.value,
|
|
10
|
+
ogTitle: () => title.value,
|
|
11
|
+
ogDescription: () => description.value,
|
|
12
|
+
ogImage: () => image.value,
|
|
13
|
+
ogType: "website",
|
|
14
|
+
...options.noindex ? { robots: "noindex, follow" } : {}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useAsyncData } from "#imports";
|
|
3
|
+
import { useCmsClient } from "./useCmsClient.js";
|
|
4
|
+
export function useCategories() {
|
|
5
|
+
const client = useCmsClient();
|
|
6
|
+
const { data, status, error } = useAsyncData(
|
|
7
|
+
"cms-categories",
|
|
8
|
+
() => client.getCategories(),
|
|
9
|
+
{ dedupe: "defer" }
|
|
10
|
+
);
|
|
11
|
+
const categories = computed(() => data.value ?? []);
|
|
12
|
+
const flatCategories = computed(() => {
|
|
13
|
+
const result = [];
|
|
14
|
+
function flatten(nodes) {
|
|
15
|
+
for (const node of nodes) {
|
|
16
|
+
result.push(node);
|
|
17
|
+
if (node.children?.length) flatten(node.children);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
flatten(categories.value);
|
|
21
|
+
return result;
|
|
22
|
+
});
|
|
23
|
+
function findBySlug(slug) {
|
|
24
|
+
return flatCategories.value.find((c) => c.slug === slug);
|
|
25
|
+
}
|
|
26
|
+
return { categories, flatCategories, findBySlug, status, error };
|
|
27
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CmsClient } from "@vigilkids/cms-client";
|
|
2
|
+
import { useRuntimeConfig, useState } from "#imports";
|
|
3
|
+
export function useCmsClient() {
|
|
4
|
+
const config = useRuntimeConfig();
|
|
5
|
+
const previewToken = useState("cmsPreviewToken", () => null);
|
|
6
|
+
const cmsConfig = config.public.cms;
|
|
7
|
+
return new CmsClient({
|
|
8
|
+
baseUrl: cmsConfig.apiUrl,
|
|
9
|
+
productCode: cmsConfig.productCode,
|
|
10
|
+
defaultLocale: cmsConfig.defaultLocale,
|
|
11
|
+
previewToken: previewToken.value ?? void 0
|
|
12
|
+
});
|
|
13
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useRuntimeConfig, useState } from "#imports";
|
|
3
|
+
export function useCmsPreview() {
|
|
4
|
+
const previewToken = useState("cmsPreviewToken", () => null);
|
|
5
|
+
const isPreview = computed(() => !!previewToken.value);
|
|
6
|
+
function exitPreview() {
|
|
7
|
+
previewToken.value = null;
|
|
8
|
+
if (import.meta.client) {
|
|
9
|
+
const config = useRuntimeConfig();
|
|
10
|
+
const cookieName = config.public.cms.preview?.cookieName ?? "__preview_token";
|
|
11
|
+
document.cookie = `${cookieName}=; path=/; max-age=0`;
|
|
12
|
+
window.location.reload();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return { isPreview, previewToken, exitPreview };
|
|
16
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useCookie, useRuntimeConfig, useState } from "#imports";
|
|
2
|
+
export default defineNuxtPlugin(() => {
|
|
3
|
+
const config = useRuntimeConfig();
|
|
4
|
+
const cookieName = config.public.cms.preview?.cookieName ?? "__preview_token";
|
|
5
|
+
const previewToken = useState("cmsPreviewToken", () => null);
|
|
6
|
+
const cookie = useCookie(cookieName);
|
|
7
|
+
if (cookie.value) {
|
|
8
|
+
previewToken.value = cookie.value;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createError, defineEventHandler, getHeader, readRawBody } from "h3";
|
|
2
|
+
import { useRuntimeConfig, useStorage } from "#imports";
|
|
3
|
+
import { verifyWebhookSignature } from "@vigilkids/cms-client/webhook";
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
const secret = config.cms.webhookSecret;
|
|
7
|
+
if (!secret) {
|
|
8
|
+
throw createError({ statusCode: 500, message: "Webhook secret not configured" });
|
|
9
|
+
}
|
|
10
|
+
const rawBody = await readRawBody(event);
|
|
11
|
+
if (!rawBody) {
|
|
12
|
+
throw createError({ statusCode: 400, message: "Empty request body" });
|
|
13
|
+
}
|
|
14
|
+
const signature = getHeader(event, "x-webhook-signature");
|
|
15
|
+
if (!signature) {
|
|
16
|
+
throw createError({ statusCode: 401, message: "Missing webhook signature" });
|
|
17
|
+
}
|
|
18
|
+
if (!verifyWebhookSignature({ rawBody, signature, secret })) {
|
|
19
|
+
throw createError({ statusCode: 401, message: "Invalid webhook signature" });
|
|
20
|
+
}
|
|
21
|
+
const payload = JSON.parse(rawBody);
|
|
22
|
+
const paths = [];
|
|
23
|
+
const defaultLocale = config.public.cms.defaultLocale ?? "en";
|
|
24
|
+
const localePrefix = payload.locale && payload.locale !== defaultLocale ? `/${payload.locale}` : "";
|
|
25
|
+
if (payload.category_slug) {
|
|
26
|
+
paths.push(`${localePrefix}/${payload.category_slug}/${payload.slug}`);
|
|
27
|
+
paths.push(`${localePrefix}/${payload.category_slug}`);
|
|
28
|
+
}
|
|
29
|
+
paths.push(`${localePrefix}/blog/${payload.slug}`);
|
|
30
|
+
paths.push(`${localePrefix}/blog`);
|
|
31
|
+
const storage = useStorage("cache:nitro:routes");
|
|
32
|
+
let cleared = 0;
|
|
33
|
+
for (const path of paths) {
|
|
34
|
+
const cacheKey = path.replace(/\//g, ":");
|
|
35
|
+
if (await storage.hasItem(cacheKey)) {
|
|
36
|
+
await storage.removeItem(cacheKey);
|
|
37
|
+
cleared++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { success: true, event: payload.event, paths_invalidated: paths, cleared };
|
|
41
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createError, defineEventHandler, getQuery, sendRedirect, setCookie } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const query = getQuery(event);
|
|
5
|
+
const token = query.token;
|
|
6
|
+
const slug = query.slug;
|
|
7
|
+
const locale = query.locale;
|
|
8
|
+
if (!token || !slug) {
|
|
9
|
+
throw createError({ statusCode: 400, message: "Missing token or slug parameter" });
|
|
10
|
+
}
|
|
11
|
+
const config = useRuntimeConfig();
|
|
12
|
+
const cmsConfig = config.public.cms;
|
|
13
|
+
const cookieName = cmsConfig.preview?.cookieName ?? "__preview_token";
|
|
14
|
+
const cookieMaxAge = cmsConfig.preview?.cookieMaxAge ?? 1800;
|
|
15
|
+
setCookie(event, cookieName, token, {
|
|
16
|
+
httpOnly: true,
|
|
17
|
+
secure: process.env.NODE_ENV === "production",
|
|
18
|
+
sameSite: "lax",
|
|
19
|
+
path: "/",
|
|
20
|
+
maxAge: cookieMaxAge
|
|
21
|
+
});
|
|
22
|
+
const defaultLocale = cmsConfig.defaultLocale ?? "en";
|
|
23
|
+
let prefix = "";
|
|
24
|
+
if (locale && locale !== defaultLocale) {
|
|
25
|
+
const prefixes = cmsConfig.localePrefixes ?? {};
|
|
26
|
+
prefix = "/" + (prefixes[locale] ?? locale.toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
const articlePath = cmsConfig.preview?.articlePath ?? "/blog/{slug}";
|
|
29
|
+
const path = articlePath.replace("{slug}", slug);
|
|
30
|
+
return sendRedirect(event, `${prefix}${path}`, 302);
|
|
31
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { defineEventHandler, getRequestURL, sendRedirect } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
let cachedRules = null;
|
|
4
|
+
let cacheExpiry = 0;
|
|
5
|
+
async function loadRules() {
|
|
6
|
+
const now = Date.now();
|
|
7
|
+
if (cachedRules && now < cacheExpiry) return cachedRules;
|
|
8
|
+
const config = useRuntimeConfig();
|
|
9
|
+
const cacheTtl = config.cms.redirects?.cacheTtl ?? 3e5;
|
|
10
|
+
try {
|
|
11
|
+
const { useServerCmsClient } = await import("../utils/cms-client.js");
|
|
12
|
+
const client = useServerCmsClient();
|
|
13
|
+
cachedRules = await client.getRedirects();
|
|
14
|
+
cacheExpiry = now + cacheTtl;
|
|
15
|
+
return cachedRules;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error("[cms-nuxt] \u52A0\u8F7D\u91CD\u5B9A\u5411\u89C4\u5219\u5931\u8D25", error);
|
|
18
|
+
return cachedRules ?? [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export default defineEventHandler(async (event) => {
|
|
22
|
+
const path = getRequestURL(event).pathname;
|
|
23
|
+
if (path.startsWith("/api/") || path.startsWith("/_nuxt/") || path.startsWith("/__nuxt")) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const rules = await loadRules();
|
|
27
|
+
for (const rule of rules) {
|
|
28
|
+
let matched = false;
|
|
29
|
+
if (rule.match_type === "exact" && path === rule.source_path) {
|
|
30
|
+
matched = true;
|
|
31
|
+
} else if (rule.match_type === "prefix" && path.startsWith(rule.source_path)) {
|
|
32
|
+
matched = true;
|
|
33
|
+
}
|
|
34
|
+
if (matched) {
|
|
35
|
+
return sendRedirect(event, rule.destination_path, rule.status_code);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CmsClient } from "@vigilkids/cms-client";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
let _client = null;
|
|
4
|
+
export function useServerCmsClient() {
|
|
5
|
+
if (!_client) {
|
|
6
|
+
const config = useRuntimeConfig();
|
|
7
|
+
const publicCms = config.public.cms;
|
|
8
|
+
_client = new CmsClient({
|
|
9
|
+
baseUrl: publicCms.apiUrl,
|
|
10
|
+
productCode: publicCms.productCode,
|
|
11
|
+
defaultLocale: publicCms.defaultLocale
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return _client;
|
|
15
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CmsClient } from "@vigilkids/cms-client";
|
|
2
|
+
export async function sitemapProvider(options) {
|
|
3
|
+
const client = new CmsClient({
|
|
4
|
+
baseUrl: options.apiUrl,
|
|
5
|
+
productCode: options.productCode,
|
|
6
|
+
defaultLocale: options.defaultLocale
|
|
7
|
+
});
|
|
8
|
+
const locales = options.sitemap?.locales ?? [options.defaultLocale ?? "en"];
|
|
9
|
+
const entries = [];
|
|
10
|
+
for (const locale of locales) {
|
|
11
|
+
const sitemapEntries = await client.getSitemap(locale);
|
|
12
|
+
for (const entry of sitemapEntries) {
|
|
13
|
+
entries.push({
|
|
14
|
+
loc: entry.loc,
|
|
15
|
+
lastmod: entry.last_mod,
|
|
16
|
+
changefreq: entry.change_freq,
|
|
17
|
+
priority: entry.priority,
|
|
18
|
+
alternatives: entry.hreflang?.map((h) => ({
|
|
19
|
+
hreflang: h.lang,
|
|
20
|
+
href: h.href
|
|
21
|
+
}))
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return entries;
|
|
26
|
+
}
|
package/dist/types.d.mts
ADDED
package/dist/types.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vigilkids/cms-nuxt",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Nuxt 3 module for OnEx CMS \u2014 composables, server routes, middleware, sitemap",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/module.mjs",
|
|
7
|
+
"types": "./dist/types.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/module.mjs",
|
|
11
|
+
"types": "./dist/types.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "nuxt-module-build build",
|
|
20
|
+
"prepack": "nuxt-module-build build"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/1yhy/onex.git",
|
|
28
|
+
"directory": "packages/cms-nuxt"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@nuxt/kit": "^3.15.0",
|
|
32
|
+
"@nuxt/module-builder": "^0.8.0",
|
|
33
|
+
"@nuxt/schema": "^3.15.0",
|
|
34
|
+
"@types/node": "^22.0.0",
|
|
35
|
+
"h3": "^1.12.0",
|
|
36
|
+
"vue": "^3.4.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@vigilkids/cms-client": "^0.0.1",
|
|
40
|
+
"nuxt": ">=3.15.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"@nuxtjs/sitemap": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|