@vigilkids/cms-nuxt 0.2.3 → 0.4.0

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +239 -22
  3. package/dist/module.d.mts +37 -2
  4. package/dist/module.d.ts +5 -44
  5. package/dist/module.d.ts.map +1 -0
  6. package/dist/module.json +1 -1
  7. package/dist/module.mjs +91 -5
  8. package/dist/runtime/composables/useArticle.d.ts +16 -0
  9. package/dist/runtime/composables/useArticle.d.ts.map +1 -0
  10. package/dist/runtime/composables/useArticle.js +1 -25
  11. package/dist/runtime/composables/useArticleList.d.ts +11 -0
  12. package/dist/runtime/composables/useArticleList.d.ts.map +1 -0
  13. package/dist/runtime/composables/useAuthor.d.ts +10 -0
  14. package/dist/runtime/composables/useAuthor.d.ts.map +1 -0
  15. package/dist/runtime/composables/useBlogSeo.d.ts +9 -0
  16. package/dist/runtime/composables/useBlogSeo.d.ts.map +1 -0
  17. package/dist/runtime/composables/useCategories.d.ts +10 -0
  18. package/dist/runtime/composables/useCategories.d.ts.map +1 -0
  19. package/dist/runtime/composables/useCmsArticleCollection.d.ts +14 -0
  20. package/dist/runtime/composables/useCmsArticleCollection.d.ts.map +1 -0
  21. package/dist/runtime/composables/useCmsArticleCollection.js +16 -0
  22. package/dist/runtime/composables/useCmsCategories.d.ts +78 -0
  23. package/dist/runtime/composables/useCmsCategories.d.ts.map +1 -0
  24. package/dist/runtime/composables/useCmsCategories.js +60 -0
  25. package/dist/runtime/composables/useCmsClient.d.ts +4 -0
  26. package/dist/runtime/composables/useCmsClient.d.ts.map +1 -0
  27. package/dist/runtime/composables/useCmsFetch.d.ts +19 -0
  28. package/dist/runtime/composables/useCmsFetch.d.ts.map +1 -0
  29. package/dist/runtime/composables/useCmsFetch.js +20 -0
  30. package/dist/runtime/composables/useCmsPreview.d.ts +16 -0
  31. package/dist/runtime/composables/useCmsPreview.d.ts.map +1 -0
  32. package/dist/runtime/composables/useCmsRoutes.d.ts +7 -0
  33. package/dist/runtime/composables/useCmsRoutes.d.ts.map +1 -0
  34. package/dist/runtime/composables/useCmsRoutes.js +10 -0
  35. package/dist/runtime/composables/useHreflangLinks.d.ts +34 -0
  36. package/dist/runtime/composables/useHreflangLinks.d.ts.map +1 -0
  37. package/dist/runtime/composables/useHreflangLinks.js +31 -0
  38. package/dist/runtime/plugins/preview-init.client.d.ts +4 -0
  39. package/dist/runtime/plugins/preview-init.client.d.ts.map +1 -0
  40. package/dist/runtime/server/api/_revalidate.post.d.ts +9 -0
  41. package/dist/runtime/server/api/_revalidate.post.d.ts.map +1 -0
  42. package/dist/runtime/server/api/_revalidate.post.js +30 -10
  43. package/dist/runtime/server/api/articles.get.d.ts +4 -0
  44. package/dist/runtime/server/api/articles.get.d.ts.map +1 -0
  45. package/dist/runtime/server/api/articles.get.js +92 -0
  46. package/dist/runtime/server/api/preview.get.d.ts +4 -0
  47. package/dist/runtime/server/api/preview.get.d.ts.map +1 -0
  48. package/dist/runtime/server/api/preview.get.js +47 -12
  49. package/dist/runtime/server/api/sitemap-urls.get.d.ts +16 -0
  50. package/dist/runtime/server/api/sitemap-urls.get.d.ts.map +1 -0
  51. package/dist/runtime/server/api/sitemap-urls.get.js +20 -0
  52. package/dist/runtime/server/middleware/preview-context.d.ts +14 -0
  53. package/dist/runtime/server/middleware/preview-context.d.ts.map +1 -0
  54. package/dist/runtime/server/middleware/redirects.d.ts +4 -0
  55. package/dist/runtime/server/middleware/redirects.d.ts.map +1 -0
  56. package/dist/runtime/server/middleware/redirects.js +27 -10
  57. package/dist/runtime/server/routes/cms-proxy.d.ts +3 -0
  58. package/dist/runtime/server/routes/cms-proxy.d.ts.map +1 -0
  59. package/dist/runtime/server/routes/cms-proxy.js +55 -0
  60. package/dist/runtime/server/utils/cms-client.d.ts +4 -0
  61. package/dist/runtime/server/utils/cms-client.d.ts.map +1 -0
  62. package/dist/runtime/server/utils/cms-client.js +14 -7
  63. package/dist/runtime/sitemap/provider.d.ts +17 -0
  64. package/dist/runtime/sitemap/provider.d.ts.map +1 -0
  65. package/dist/runtime/sitemap/provider.js +9 -11
  66. package/dist/runtime/utils/list.d.ts +2 -0
  67. package/dist/runtime/utils/list.d.ts.map +1 -0
  68. package/dist/runtime/utils/routes.d.ts +19 -0
  69. package/dist/runtime/utils/routes.d.ts.map +1 -0
  70. package/dist/runtime/utils/routes.js +119 -0
  71. package/dist/runtime/utils/sitemap.d.ts +20 -0
  72. package/dist/runtime/utils/sitemap.d.ts.map +1 -0
  73. package/dist/runtime/utils/sitemap.js +44 -0
  74. package/dist/types.d.ts +76 -7
  75. package/dist/types.d.ts.map +1 -0
  76. package/package.json +7 -6
@@ -1,4 +1,4 @@
1
- import { useAsyncData, useHead, useRuntimeConfig, useSeoMeta } from "#imports";
1
+ import { useAsyncData, useHead, useSeoMeta } from "#imports";
2
2
  import { computed, toRef } from "vue";
3
3
  import { toArray } from "../utils/list.js";
4
4
  import { useCmsClient } from "./useCmsClient.js";
@@ -38,30 +38,6 @@ export function useArticle(slug) {
38
38
  return [{ type: "application/ld+json", innerHTML: JSON.stringify(article.value.json_ld) }];
39
39
  }
40
40
  });
41
- useHead({
42
- link: () => {
43
- const a = article.value;
44
- if (!a?.available_locales || a.available_locales.length <= 1)
45
- return [];
46
- const publicConfig = useRuntimeConfig().public;
47
- const siteUrl = publicConfig.siteUrl ?? "";
48
- const defaultSlug = a.localized_slugs?.en ?? a.slug;
49
- const links = a.available_locales.map((locale) => {
50
- const slug2 = a.localized_slugs?.[locale] ?? a.slug;
51
- return {
52
- rel: "alternate",
53
- hreflang: locale,
54
- href: locale === "en" ? `${siteUrl}/blog/${slug2}` : `${siteUrl}/${locale}/blog/${slug2}`
55
- };
56
- });
57
- links.push({
58
- rel: "alternate",
59
- hreflang: "x-default",
60
- href: `${siteUrl}/blog/${defaultSlug}`
61
- });
62
- return links;
63
- }
64
- });
65
41
  return {
66
42
  article,
67
43
  relatedArticles: relatedArticleList,
@@ -0,0 +1,11 @@
1
+ import type { ArticleListItem, ArticleListParams, PaginationMeta } from '@vigilkids/cms-client/types';
2
+ import type { MaybeRefOrGetter } from 'vue';
3
+ /** 文章列表 + 分页 */
4
+ export declare function useArticleList(params: MaybeRefOrGetter<ArticleListParams>): {
5
+ articles: import("vue").ComputedRef<ArticleListItem[]>;
6
+ pagination: import("vue").ComputedRef<PaginationMeta | null>;
7
+ status: import("#imports").Ref<string, string>;
8
+ error: import("#imports").Ref<Error | null, Error | null>;
9
+ refresh: () => Promise<void>;
10
+ };
11
+ //# sourceMappingURL=useArticleList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useArticleList.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useArticleList.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,cAAc,EACf,MAAM,6BAA6B,CAAA;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,KAAK,CAAA;AAK3C,gBAAgB;AAChB,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,CAAC,iBAAiB,CAAC;;;;;;EAczE"}
@@ -0,0 +1,10 @@
1
+ import type { ArticleListItem, Author } from '@vigilkids/cms-client/types';
2
+ import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue';
3
+ /** 作者详情 + 文章列表 */
4
+ export declare function useAuthor(slug: MaybeRefOrGetter<string>): {
5
+ author: Ref<Author | null>;
6
+ articles: ComputedRef<ArticleListItem[]>;
7
+ status: Ref<string, string>;
8
+ error: Ref<Error | null, Error | null>;
9
+ };
10
+ //# sourceMappingURL=useAuthor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAuthor.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useAuthor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAA;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAM7D,kBAAkB;AAClB,wBAAgB,SAAS,CAAC,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC;YAoBlC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC;cACX,WAAW,CAAC,eAAe,EAAE,CAAC;;;EAI1D"}
@@ -0,0 +1,9 @@
1
+ import type { MaybeRefOrGetter } from 'vue';
2
+ /** 博客列表页 SEO */
3
+ export declare function useBlogSeo(options: {
4
+ title: MaybeRefOrGetter<string>;
5
+ description?: MaybeRefOrGetter<string>;
6
+ image?: MaybeRefOrGetter<string | undefined>;
7
+ noindex?: boolean;
8
+ }): void;
9
+ //# sourceMappingURL=useBlogSeo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBlogSeo.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useBlogSeo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,KAAK,CAAA;AAI3C,gBAAgB;AAChB,wBAAgB,UAAU,CAAC,OAAO,EAAE;IAClC,KAAK,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAA;IAC/B,WAAW,CAAC,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAA;IACtC,KAAK,CAAC,EAAE,gBAAgB,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAC5C,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,QAcA"}
@@ -0,0 +1,10 @@
1
+ import type { CategoryNode } from '@vigilkids/cms-client/types';
2
+ /** 分类树 + 工具方法 */
3
+ export declare function useCategories(): {
4
+ categories: import("vue").ComputedRef<CategoryNode[]>;
5
+ flatCategories: import("vue").ComputedRef<CategoryNode[]>;
6
+ findBySlug: (slug: string) => CategoryNode | undefined;
7
+ status: import("#imports").Ref<string, string>;
8
+ error: import("#imports").Ref<Error | null, Error | null>;
9
+ };
10
+ //# sourceMappingURL=useCategories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCategories.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useCategories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAM/D,iBAAiB;AACjB,wBAAgB,aAAa;;;uBAsBD,MAAM,KAAG,YAAY,GAAG,SAAS;;;EAK5D"}
@@ -0,0 +1,14 @@
1
+ import type { ArticleListItem, ArticleListParams, PaginationMeta } from '@vigilkids/cms-client/types';
2
+ import type { MaybeRefOrGetter } from 'vue';
3
+ export interface CmsArticleCollectionParams extends ArticleListParams {
4
+ q?: string;
5
+ }
6
+ /** CMS 文章集合查询,统一支持分类、搜索和分页 */
7
+ export declare function useCmsArticleCollection(params: MaybeRefOrGetter<CmsArticleCollectionParams>): {
8
+ articles: import("vue").ComputedRef<ArticleListItem[]>;
9
+ pagination: import("vue").ComputedRef<PaginationMeta | null>;
10
+ status: import("#imports").Ref<string, string>;
11
+ error: import("#imports").Ref<Error | null, Error | null>;
12
+ refresh: () => Promise<void>;
13
+ };
14
+ //# sourceMappingURL=useCmsArticleCollection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCmsArticleCollection.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useCmsArticleCollection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAa,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAChH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,KAAK,CAAA;AAI3C,MAAM,WAAW,0BAA2B,SAAQ,iBAAiB;IACnE,CAAC,CAAC,EAAE,MAAM,CAAA;CACX;AAED,8BAA8B;AAC9B,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,gBAAgB,CAAC,0BAA0B,CAAC;;;;;;EAkB3F"}
@@ -0,0 +1,16 @@
1
+ import { useAsyncData, useRuntimeConfig } from "#imports";
2
+ import { computed, toRef } from "vue";
3
+ export function useCmsArticleCollection(params) {
4
+ const resolvedParams = toRef(params);
5
+ const config = useRuntimeConfig();
6
+ const cmsConfig = config.public.cms;
7
+ const endpoint = cmsConfig.content?.articleCollectionPath ?? "/api/cms/articles";
8
+ const { data, status, error, refresh } = useAsyncData(
9
+ `cms-article-collection-${JSON.stringify(resolvedParams.value)}`,
10
+ () => $fetch(endpoint, { params: resolvedParams.value }),
11
+ { watch: [resolvedParams] }
12
+ );
13
+ const articles = computed(() => data.value?.data ?? []);
14
+ const pagination = computed(() => data.value?.pagination ?? null);
15
+ return { articles, pagination, status, error, refresh };
16
+ }
@@ -0,0 +1,78 @@
1
+ import type { CategoryNode } from '@vigilkids/cms-client/types';
2
+ export interface CmsResolvedCategory {
3
+ id: number;
4
+ slug: string;
5
+ name: string;
6
+ description?: string;
7
+ articleCount: number;
8
+ }
9
+ /** CMS 分类预加载数据和路由工具 */
10
+ export declare function useCmsCategories(): {
11
+ rawCategories: Readonly<import("#imports").Ref<readonly {
12
+ readonly id: number;
13
+ readonly slug: string;
14
+ readonly name: {
15
+ readonly [x: string]: string;
16
+ };
17
+ readonly description: {
18
+ readonly [x: string]: string;
19
+ };
20
+ readonly article_count: number;
21
+ readonly article_counts?: {
22
+ readonly [x: string]: number;
23
+ } | undefined;
24
+ readonly children?: readonly /*elided*/ any[] | undefined;
25
+ }[], readonly {
26
+ readonly id: number;
27
+ readonly slug: string;
28
+ readonly name: {
29
+ readonly [x: string]: string;
30
+ };
31
+ readonly description: {
32
+ readonly [x: string]: string;
33
+ };
34
+ readonly article_count: number;
35
+ readonly article_counts?: {
36
+ readonly [x: string]: number;
37
+ } | undefined;
38
+ readonly children?: readonly /*elided*/ any[] | undefined;
39
+ }[]>>;
40
+ flatCategories: Readonly<import("#imports").Ref<readonly {
41
+ readonly id: number;
42
+ readonly slug: string;
43
+ readonly name: {
44
+ readonly [x: string]: string;
45
+ };
46
+ readonly description: {
47
+ readonly [x: string]: string;
48
+ };
49
+ readonly article_count: number;
50
+ readonly article_counts?: {
51
+ readonly [x: string]: number;
52
+ } | undefined;
53
+ readonly children?: readonly /*elided*/ any[] | undefined;
54
+ }[], readonly {
55
+ readonly id: number;
56
+ readonly slug: string;
57
+ readonly name: {
58
+ readonly [x: string]: string;
59
+ };
60
+ readonly description: {
61
+ readonly [x: string]: string;
62
+ };
63
+ readonly article_count: number;
64
+ readonly article_counts?: {
65
+ readonly [x: string]: number;
66
+ } | undefined;
67
+ readonly children?: readonly /*elided*/ any[] | undefined;
68
+ }[]>>;
69
+ currentCategories: import("vue").ComputedRef<CmsResolvedCategory[]>;
70
+ lastUpdated: string | undefined;
71
+ buildTime: string | undefined;
72
+ resolveCategory: (category: CategoryNode, locale?: string) => CmsResolvedCategory;
73
+ getCategoryPath: (category: Pick<CategoryNode, "slug">) => string;
74
+ getDisplayCategories: (locale?: string) => CmsResolvedCategory[];
75
+ categoryExists: (slug: string) => boolean;
76
+ findBySlug: (slug: string) => CategoryNode | undefined;
77
+ };
78
+ //# sourceMappingURL=useCmsCategories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCmsCategories.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useCmsCategories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAK/D,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;CACrB;AAqBD,uBAAuB;AACvB,wBAAgB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAcK,YAAY,sBAAkB,mBAAmB;gCAkBjD,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC;;2BAJ/B,MAAM;uBAJV,MAAM;EA4BjC"}
@@ -0,0 +1,60 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import { computed, readonly } from "vue";
3
+ import { useCmsRoutes } from "./useCmsRoutes.js";
4
+ function localizedText(value, locale, fallback) {
5
+ if (!value)
6
+ return fallback;
7
+ return value[locale] || value.en || Object.values(value)[0] || fallback;
8
+ }
9
+ function flattenCategories(categories) {
10
+ const result = [];
11
+ function walk(nodes) {
12
+ for (const node of nodes) {
13
+ result.push(node);
14
+ if (node.children?.length)
15
+ walk(node.children);
16
+ }
17
+ }
18
+ walk(categories);
19
+ return result;
20
+ }
21
+ export function useCmsCategories() {
22
+ const config = useRuntimeConfig();
23
+ const routes = useCmsRoutes();
24
+ const cmsConfig = config.public.cms;
25
+ const rawCategories = computed(() => cmsConfig.categories?.items ?? []);
26
+ const flatCategories = computed(() => flattenCategories(rawCategories.value));
27
+ function resolveCategory(category, locale = "en") {
28
+ return {
29
+ id: category.id,
30
+ slug: category.slug,
31
+ name: localizedText(category.name, locale, category.slug),
32
+ description: localizedText(category.description, locale, ""),
33
+ articleCount: category.article_count
34
+ };
35
+ }
36
+ function findBySlug(slug) {
37
+ return flatCategories.value.find((category) => category.slug === slug);
38
+ }
39
+ function categoryExists(slug) {
40
+ return Boolean(findBySlug(slug));
41
+ }
42
+ function getCategoryPath(category) {
43
+ return routes.build("category", { category_slug: category.slug });
44
+ }
45
+ function getDisplayCategories(locale = "en") {
46
+ return rawCategories.value.map((category) => resolveCategory(category, locale));
47
+ }
48
+ return {
49
+ rawCategories: readonly(rawCategories),
50
+ flatCategories: readonly(flatCategories),
51
+ currentCategories: computed(() => getDisplayCategories()),
52
+ lastUpdated: cmsConfig.categories?.lastUpdated,
53
+ buildTime: cmsConfig.categories?.buildTime,
54
+ resolveCategory,
55
+ getCategoryPath,
56
+ getDisplayCategories,
57
+ categoryExists,
58
+ findBySlug
59
+ };
60
+ }
@@ -0,0 +1,4 @@
1
+ import { CmsClient } from '@vigilkids/cms-client';
2
+ /** 获取 CMS 客户端实例(SSR/CSR 通用) */
3
+ export declare function useCmsClient(): CmsClient;
4
+ //# sourceMappingURL=useCmsClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCmsClient.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useCmsClient.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAEjD,+BAA+B;AAC/B,wBAAgB,YAAY,IAAI,SAAS,CAexC"}
@@ -0,0 +1,19 @@
1
+ import type { MaybeRefOrGetter, Ref } from 'vue';
2
+ export interface CmsFetchError {
3
+ statusCode?: number;
4
+ statusMessage?: string;
5
+ message?: string;
6
+ [key: string]: unknown;
7
+ }
8
+ export interface CmsFetchResult<T> {
9
+ data: Ref<T | null>;
10
+ error: Ref<CmsFetchError | null>;
11
+ status: Ref<string>;
12
+ refresh: () => Promise<void>;
13
+ execute: () => Promise<void>;
14
+ }
15
+ /** CMS API 请求封装,SSR 直连 CMS,CSR 走 Nuxt 代理。 */
16
+ export declare function useCmsFetch<T>(path: MaybeRefOrGetter<string>, options?: Record<string, unknown> & {
17
+ headers?: Record<string, string>;
18
+ }): CmsFetchResult<T>;
19
+ //# sourceMappingURL=useCmsFetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCmsFetch.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useCmsFetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAIhD,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,IAAI,EAAE,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;IACnB,KAAK,EAAE,GAAG,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IAChC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACnB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B;AAED,6CAA6C;AAC7C,wBAAgB,WAAW,CAAC,CAAC,EAC3B,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACvE,cAAc,CAAC,CAAC,CAAC,CAyBnB"}
@@ -0,0 +1,20 @@
1
+ import { useFetch, useRuntimeConfig } from "#imports";
2
+ import { useCmsPreviewToken } from "./useCmsPreview.js";
3
+ export function useCmsFetch(path, options) {
4
+ const config = useRuntimeConfig();
5
+ const cmsConfig = config.public.cms;
6
+ const baseURL = import.meta.server ? cmsConfig.apiUrl : cmsConfig.content?.cmsProxyPath ?? "/cms-proxy";
7
+ const headers = { Accept: "application/json" };
8
+ if (cmsConfig.productCode) {
9
+ headers["X-Product-Code"] = cmsConfig.productCode;
10
+ }
11
+ const previewToken = useCmsPreviewToken();
12
+ if (previewToken.value) {
13
+ headers["X-Preview-Token"] = previewToken.value;
14
+ }
15
+ return useFetch(path, {
16
+ ...options,
17
+ baseURL,
18
+ headers: { ...headers, ...options?.headers }
19
+ });
20
+ }
@@ -0,0 +1,16 @@
1
+ import type { Ref } from 'vue';
2
+ /**
3
+ * 获取预览 token(SWR 安全)。
4
+ *
5
+ * SSR 时从 event.context.__cms_preview_token 读取,该值由 preview-context
6
+ * 中间件写入,通过引用共享穿越 Nitro SWR 合成事件边界。
7
+ * CSR 时从 cookie 读取(客户端不受 SWR 影响)。
8
+ */
9
+ export declare function useCmsPreviewToken(): Ref<string | null>;
10
+ /** 预览模式管理 */
11
+ export declare function useCmsPreview(): {
12
+ isPreview: import("vue").ComputedRef<boolean>;
13
+ previewToken: Ref<string | null, string | null>;
14
+ exitPreview: () => void;
15
+ };
16
+ //# sourceMappingURL=useCmsPreview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCmsPreview.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useCmsPreview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAI9B;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,CAgBvD;AAED,aAAa;AACb,wBAAgB,aAAa;;;;EAkB5B"}
@@ -0,0 +1,7 @@
1
+ import type { CmsRouteParams } from '../../types';
2
+ /** CMS 资源路由生成器 */
3
+ export declare function useCmsRoutes(): {
4
+ policy: Required<import("../../types").CmsRoutePolicy>;
5
+ build: (resource: string, params?: CmsRouteParams, includeLocale?: boolean) => string;
6
+ };
7
+ //# sourceMappingURL=useCmsRoutes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCmsRoutes.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useCmsRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,kBAAkB;AAClB,wBAAgB,YAAY;;sBAID,MAAM,WAAU,cAAc;EAKxD"}
@@ -0,0 +1,10 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import { buildCmsPath, resolveCmsRoutePolicy } from "../utils/routes.js";
3
+ export function useCmsRoutes() {
4
+ const config = useRuntimeConfig();
5
+ const policy = resolveCmsRoutePolicy(config.public.cms);
6
+ function build(resource, params = {}, includeLocale = false) {
7
+ return buildCmsPath(policy, resource, params, { includeLocale });
8
+ }
9
+ return { policy, build };
10
+ }
@@ -0,0 +1,34 @@
1
+ import type { MaybeRefOrGetter } from 'vue';
2
+ /** 任何具备多语言能力的 CMS 资源应满足的最小契约。 */
3
+ export interface HreflangResource {
4
+ slug: string;
5
+ available_locales?: string[];
6
+ localized_slugs?: Record<string, string>;
7
+ }
8
+ export interface HreflangBuilderParams<T extends HreflangResource> {
9
+ /** 当前 locale code;包内对 x-default 单独再调用一次,传入字面量 'x-default'。 */
10
+ locale: string | 'x-default';
11
+ /** 对应 locale 的本地化 slug;无本地化时回退到资源原 slug。 */
12
+ slug: string;
13
+ /** 完整资源对象,builder 可读取 category / author 等领域字段拼路径。 */
14
+ resource: T;
15
+ }
16
+ /**
17
+ * 按资源的 available_locales 注入 hreflang `<link>` 标签。
18
+ *
19
+ * 包内只负责"按 available_locales 过滤 + 添加 x-default 入口";URL 路径结构、
20
+ * locale 前缀策略、trailing slash、siteUrl 拼接全部交由 builder 决定,
21
+ * 因此可适配任意资源路由模式(/{category}/{slug}、/authors/{slug} 等)。
22
+ *
23
+ * 每条 link 带上 `key: cms-hreflang-{locale}`,便于 unhead 跨多次调用去重。
24
+ *
25
+ * @example
26
+ * useHreflangLinks(article, ({ locale, slug, resource }) => {
27
+ * const origin = `https://${siteUrl}`
28
+ * if (locale === 'x-default') return `${origin}/${resource.category}/${slug}/`
29
+ * const prefix = locale === 'en' ? '' : `/${locale}`
30
+ * return `${origin}${prefix}/${resource.category}/${slug}/`
31
+ * })
32
+ */
33
+ export declare function useHreflangLinks<T extends HreflangResource>(resource: MaybeRefOrGetter<T | null | undefined>, builder: (params: HreflangBuilderParams<T>) => string): void;
34
+ //# sourceMappingURL=useHreflangLinks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useHreflangLinks.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useHreflangLinks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,KAAK,CAAA;AAI3C,iCAAiC;AACjC,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACzC;AAED,MAAM,WAAW,qBAAqB,CAAC,CAAC,SAAS,gBAAgB;IAC/D,8DAA8D;IAC9D,MAAM,EAAE,MAAM,GAAG,WAAW,CAAA;IAC5B,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAA;IACZ,qDAAqD;IACrD,QAAQ,EAAE,CAAC,CAAA;CACZ;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,gBAAgB,EACzD,QAAQ,EAAE,gBAAgB,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,EAChD,OAAO,EAAE,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC,KAAK,MAAM,QAgCtD"}
@@ -0,0 +1,31 @@
1
+ import { useHead } from "#imports";
2
+ import { toValue } from "vue";
3
+ export function useHreflangLinks(resource, builder) {
4
+ useHead({
5
+ link: () => {
6
+ const r = toValue(resource);
7
+ const locales = r?.available_locales;
8
+ if (!r || !locales || locales.length === 0)
9
+ return [];
10
+ const localized = r.localized_slugs ?? {};
11
+ const links = locales.map((locale) => ({
12
+ rel: "alternate",
13
+ hreflang: locale,
14
+ href: builder({
15
+ locale,
16
+ slug: localized[locale] ?? r.slug,
17
+ resource: r
18
+ }),
19
+ key: `cms-hreflang-${locale}`
20
+ }));
21
+ const defaultSlug = localized.en ?? localized[locales[0]] ?? r.slug;
22
+ links.push({
23
+ rel: "alternate",
24
+ hreflang: "x-default",
25
+ href: builder({ locale: "x-default", slug: defaultSlug, resource: r }),
26
+ key: "cms-hreflang-x-default"
27
+ });
28
+ return links;
29
+ }
30
+ });
31
+ }
@@ -0,0 +1,4 @@
1
+ /** 从 cookie 恢复预览 token */
2
+ declare const _default: unknown;
3
+ export default _default;
4
+ //# sourceMappingURL=preview-init.client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-init.client.d.ts","sourceRoot":"","sources":["../../../src/runtime/plugins/preview-init.client.ts"],"names":[],"mappings":"AAEA,0BAA0B;;AAC1B,wBAUE"}
@@ -0,0 +1,9 @@
1
+ /** ISR 缓存失效端点 */
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
3
+ success: boolean;
4
+ event: import("@vigilkids/cms-client").WebhookEventType;
5
+ paths_invalidated: string[];
6
+ cleared: number;
7
+ }>>;
8
+ export default _default;
9
+ //# sourceMappingURL=_revalidate.post.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_revalidate.post.d.ts","sourceRoot":"","sources":["../../../../src/runtime/server/api/_revalidate.post.ts"],"names":[],"mappings":"AAiCA,iBAAiB;;;;;;;AACjB,wBA4DE"}
@@ -1,6 +1,19 @@
1
1
  import { useRuntimeConfig, useStorage } from "#imports";
2
2
  import { verifyWebhookSignature } from "@vigilkids/cms-client/webhook";
3
3
  import { createError, defineEventHandler, getHeader, readRawBody } from "h3";
4
+ import { buildCmsPath, resolveCmsRoutePolicy } from "../../utils/routes.js";
5
+ function addPath(paths, resource, routePolicy, params) {
6
+ try {
7
+ paths.add(buildCmsPath(routePolicy, resource, params, { includeLocale: true }));
8
+ } catch {
9
+ }
10
+ }
11
+ function publicLocale(locale, localeMap = {}) {
12
+ if (!locale)
13
+ return locale;
14
+ const match = Object.entries(localeMap).find(([, cmsLocale]) => cmsLocale === locale);
15
+ return match?.[0] ?? locale;
16
+ }
4
17
  export default defineEventHandler(async (event) => {
5
18
  const config = useRuntimeConfig();
6
19
  const secret = config.cms.webhookSecret;
@@ -19,15 +32,22 @@ export default defineEventHandler(async (event) => {
19
32
  throw createError({ statusCode: 401, message: "Invalid webhook signature" });
20
33
  }
21
34
  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`);
35
+ const cmsConfig = config.public.cms;
36
+ const routePolicy = resolveCmsRoutePolicy(cmsConfig);
37
+ const routeParams = {
38
+ ...payload.route_params ?? {},
39
+ locale: publicLocale(payload.locale, cmsConfig.localeMap),
40
+ slug: payload.slug,
41
+ category_slug: payload.category_slug
42
+ };
43
+ const paths = /* @__PURE__ */ new Set();
44
+ addPath(paths, "index", routePolicy, routeParams);
45
+ if (routeParams.category_slug) {
46
+ addPath(paths, "category", routePolicy, routeParams);
47
+ }
48
+ if (routeParams.slug) {
49
+ addPath(paths, payload.resource_type || "article", routePolicy, routeParams);
50
+ }
31
51
  const storage = useStorage("cache:nitro:routes");
32
52
  const allKeys = await storage.getKeys("_:");
33
53
  let cleared = 0;
@@ -39,5 +59,5 @@ export default defineEventHandler(async (event) => {
39
59
  cleared++;
40
60
  }
41
61
  }
42
- return { success: true, event: payload.event, paths_invalidated: paths, cleared };
62
+ return { success: true, event: payload.event, paths_invalidated: [...paths], cleared };
43
63
  });
@@ -0,0 +1,4 @@
1
+ import type { ArticleListItem, Paginated } from '@vigilkids/cms-client/types';
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Paginated<ArticleListItem>>>;
3
+ export default _default;
4
+ //# sourceMappingURL=articles.get.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"articles.get.d.ts","sourceRoot":"","sources":["../../../../src/runtime/server/api/articles.get.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,eAAe,EAEf,SAAS,EACV,MAAM,6BAA6B,CAAA;;AA0DpC,wBAiDE"}
@@ -0,0 +1,92 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import { defineEventHandler, getQuery, setResponseHeader } from "h3";
3
+ import { useServerCmsClient } from "../utils/cms-client.js";
4
+ const DEFAULT_PAGE_SIZE = 20;
5
+ const MAX_PAGE_SIZE = 20;
6
+ function positiveInteger(value, fallback, max) {
7
+ const parsed = typeof value === "string" ? Number.parseInt(value, 10) : Number(value);
8
+ if (!Number.isFinite(parsed) || parsed < 1)
9
+ return fallback;
10
+ const normalized = Math.floor(parsed);
11
+ return max ? Math.min(normalized, max) : normalized;
12
+ }
13
+ function stringValue(value) {
14
+ return typeof value === "string" ? value.trim() : "";
15
+ }
16
+ function articleFromDetail(detail, fallback) {
17
+ return {
18
+ slug: detail.slug || fallback.slug,
19
+ title: detail.title || fallback.title,
20
+ excerpt: detail.excerpt || fallback.excerpt || "",
21
+ featured_image_url: detail.featured_image_url || fallback.featured_image_url || "",
22
+ featured_image_alt: detail.featured_image_alt || fallback.featured_image_alt || "",
23
+ published_at: detail.published_at ?? fallback.published_at ?? null,
24
+ last_published_at: detail.last_published_at ?? fallback.last_published_at ?? null,
25
+ reading_time_minutes: detail.reading_time_minutes || fallback.reading_time_minutes || 0,
26
+ category_slug: detail.category || fallback.category_slug,
27
+ is_pinned: false,
28
+ tags: detail.tags || fallback.tags || [],
29
+ authors: detail.authors || fallback.authors || []
30
+ };
31
+ }
32
+ function articleFromSearch(item) {
33
+ return {
34
+ slug: item.slug,
35
+ title: item.title,
36
+ excerpt: item.excerpt || "",
37
+ featured_image_url: item.featured_image_url || "",
38
+ featured_image_alt: item.featured_image_alt || "",
39
+ published_at: item.published_at ?? null,
40
+ last_published_at: item.last_published_at ?? null,
41
+ reading_time_minutes: item.reading_time_minutes || 0,
42
+ category_slug: item.category_slug,
43
+ is_pinned: false,
44
+ tags: item.tags || [],
45
+ authors: item.authors || []
46
+ };
47
+ }
48
+ function hasArticleCardFields(item) {
49
+ return Boolean(item.category_slug && item.featured_image_url && item.published_at && item.authors?.length);
50
+ }
51
+ export default defineEventHandler(async (event) => {
52
+ setResponseHeader(event, "Cache-Control", "private, no-cache, no-store");
53
+ const query = getQuery(event);
54
+ const page = positiveInteger(query.page, 1);
55
+ const pageSize = positiveInteger(query.page_size, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
56
+ const config = useRuntimeConfig();
57
+ const cmsConfig = config.public.cms;
58
+ const publicLocale = stringValue(query.locale) || cmsConfig.defaultLocale || "en";
59
+ const locale = cmsConfig.localeMap?.[publicLocale] ?? publicLocale;
60
+ const categorySlug = stringValue(query.category_slug);
61
+ const keyword = stringValue(query.q);
62
+ const client = useServerCmsClient();
63
+ if (!keyword) {
64
+ return await client.getArticles({
65
+ locale,
66
+ page,
67
+ page_size: pageSize,
68
+ sort_by: "published_at",
69
+ sort_order: "desc",
70
+ ...categorySlug ? { category_slug: categorySlug } : {}
71
+ });
72
+ }
73
+ const searchResponse = await client.searchArticles({
74
+ locale,
75
+ q: keyword,
76
+ page,
77
+ page_size: pageSize
78
+ });
79
+ const data = await Promise.all(
80
+ searchResponse.data.map(async (item) => {
81
+ if (hasArticleCardFields(item)) {
82
+ return articleFromSearch(item);
83
+ }
84
+ const detail = await client.getArticle(item.slug, locale);
85
+ return articleFromDetail(detail, item);
86
+ })
87
+ );
88
+ return {
89
+ data,
90
+ pagination: searchResponse.pagination
91
+ };
92
+ });
@@ -0,0 +1,4 @@
1
+ /** 预览入口端点:验证 token、设置预览 cookie、重定向到对应语言的文章页 */
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
3
+ export default _default;
4
+ //# sourceMappingURL=preview.get.d.ts.map