@vigilkids/cms-nuxt 0.3.0 → 0.5.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 +111 -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/useArticleList.d.ts +11 -0
  11. package/dist/runtime/composables/useArticleList.d.ts.map +1 -0
  12. package/dist/runtime/composables/useAuthor.d.ts +10 -0
  13. package/dist/runtime/composables/useAuthor.d.ts.map +1 -0
  14. package/dist/runtime/composables/useBlogSeo.d.ts +25 -0
  15. package/dist/runtime/composables/useBlogSeo.d.ts.map +1 -0
  16. package/dist/runtime/composables/useBlogSeo.js +99 -13
  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 +188 -0
  23. package/dist/runtime/composables/useCmsCategories.d.ts.map +1 -0
  24. package/dist/runtime/composables/useCmsCategories.js +71 -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 +2 -1
  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
@@ -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"}
@@ -5,7 +5,8 @@ export function useHreflangLinks(resource, builder) {
5
5
  link: () => {
6
6
  const r = toValue(resource);
7
7
  const locales = r?.available_locales;
8
- if (!r || !locales || locales.length === 0) return [];
8
+ if (!r || !locales || locales.length === 0)
9
+ return [];
9
10
  const localized = r.localized_slugs ?? {};
10
11
  const links = locales.map((locale) => ({
11
12
  rel: "alternate",
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.get.d.ts","sourceRoot":"","sources":["../../../../src/runtime/server/api/preview.get.ts"],"names":[],"mappings":"AAsBA,+CAA+C;;AAC/C,wBAiEE"}
@@ -1,16 +1,33 @@
1
1
  import { env } from "node:process";
2
2
  import { useRuntimeConfig } from "#imports";
3
3
  import { createError, defineEventHandler, getQuery, sendRedirect, setCookie } from "h3";
4
+ import { buildCmsPath, extractCmsSourceRouteParams, mergeCmsRouteParams, resolveCmsRoutePolicy } from "../../utils/routes.js";
5
+ import { createServerCmsClient } from "../utils/cms-client.js";
6
+ function stringValue(value) {
7
+ return typeof value === "string" ? value.trim() : "";
8
+ }
9
+ function queryRouteParams(query) {
10
+ const params = {};
11
+ for (const [key, value] of Object.entries(query)) {
12
+ if (typeof value === "string" && value.trim()) {
13
+ params[key] = value.trim();
14
+ }
15
+ }
16
+ return params;
17
+ }
4
18
  export default defineEventHandler(async (event) => {
5
19
  const query = getQuery(event);
6
- const token = query.token;
7
- const slug = query.slug;
8
- const locale = query.locale;
9
- if (!token || !slug) {
20
+ const token = stringValue(query.token);
21
+ const rawSlug = stringValue(query.slug);
22
+ const resource = stringValue(query.resource_type) || stringValue(query.resource) || "article";
23
+ if (!token || resource === "article" && !rawSlug) {
10
24
  throw createError({ statusCode: 400, message: "Missing token or slug parameter" });
11
25
  }
12
26
  const config = useRuntimeConfig();
13
27
  const cmsConfig = config.public.cms;
28
+ const routePolicy = resolveCmsRoutePolicy(cmsConfig);
29
+ const publicLocale = stringValue(query.locale) || routePolicy.locale.defaultLocale || "en";
30
+ const apiLocale = cmsConfig.localeMap?.[publicLocale] ?? publicLocale;
14
31
  const cookieName = cmsConfig.preview?.cookieName ?? "__preview_token";
15
32
  const cookieMaxAge = cmsConfig.preview?.cookieMaxAge ?? 1800;
16
33
  setCookie(event, cookieName, token, {
@@ -20,13 +37,31 @@ export default defineEventHandler(async (event) => {
20
37
  path: "/",
21
38
  maxAge: cookieMaxAge
22
39
  });
23
- const defaultLocale = cmsConfig.defaultLocale ?? "en";
24
- let prefix = "";
25
- if (locale && locale !== defaultLocale) {
26
- const prefixes = cmsConfig.localePrefixes ?? {};
27
- prefix = `/${prefixes[locale] ?? locale.toLowerCase()}`;
40
+ const sourceParams = rawSlug ? extractCmsSourceRouteParams(routePolicy, resource, rawSlug) : null;
41
+ let routeParams = mergeCmsRouteParams(sourceParams, queryRouteParams(query));
42
+ routeParams.locale = publicLocale;
43
+ if (rawSlug && !routeParams.slug) {
44
+ routeParams.slug = rawSlug;
45
+ }
46
+ if (resource === "article" && !routeParams.category_slug && routePolicy.resources.article?.includes("{category_slug}")) {
47
+ const client = createServerCmsClient(token);
48
+ try {
49
+ const article = await client.getArticle(String(routeParams.slug), apiLocale);
50
+ if (article.category) {
51
+ routeParams = { ...routeParams, category_slug: article.category };
52
+ }
53
+ } catch {
54
+ throw createError({ statusCode: 404, message: "Preview article not found" });
55
+ }
56
+ }
57
+ let path;
58
+ try {
59
+ path = buildCmsPath(routePolicy, resource, routeParams, { includeLocale: true });
60
+ } catch (error) {
61
+ throw createError({
62
+ statusCode: 400,
63
+ message: error instanceof Error ? error.message : "Invalid preview route parameters"
64
+ });
28
65
  }
29
- const articlePath = cmsConfig.preview?.articlePath ?? "/blog/{slug}";
30
- const path = articlePath.replace("{slug}", slug);
31
- return sendRedirect(event, `${prefix}${path}?_preview=1`, 302);
66
+ return sendRedirect(event, `${path}?_preview=1`, 302);
32
67
  });
@@ -0,0 +1,16 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ loc: string;
3
+ lastmod: string;
4
+ changefreq: string;
5
+ priority: number;
6
+ alternatives: {
7
+ hreflang: string;
8
+ href: string;
9
+ }[] | undefined;
10
+ images: {
11
+ loc: string;
12
+ title: string | undefined;
13
+ }[] | undefined;
14
+ }[]>>;
15
+ export default _default;
16
+ //# sourceMappingURL=sitemap-urls.get.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap-urls.get.d.ts","sourceRoot":"","sources":["../../../../src/runtime/server/api/sitemap-urls.get.ts"],"names":[],"mappings":";;;;;;;;;;;;;;AASA,wBAqBE"}
@@ -0,0 +1,20 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import { defineEventHandler, getQuery, setResponseHeader } from "h3";
3
+ import { toArray } from "../../utils/list.js";
4
+ import { resolveCmsRoutePolicy } from "../../utils/routes.js";
5
+ import { cmsLocale, toNuxtSitemapEntry } from "../../utils/sitemap.js";
6
+ import { useServerCmsClient } from "../utils/cms-client.js";
7
+ export default defineEventHandler(async (event) => {
8
+ setResponseHeader(event, "Cache-Control", "private, no-cache, no-store");
9
+ const { locale } = getQuery(event);
10
+ if (!locale || typeof locale !== "string")
11
+ return [];
12
+ const config = useRuntimeConfig();
13
+ const cmsConfig = config.public.cms;
14
+ const routePolicy = resolveCmsRoutePolicy(cmsConfig);
15
+ const client = useServerCmsClient();
16
+ const entries = await client.getSitemap(cmsLocale(cmsConfig.localeMap, locale));
17
+ return toArray(entries).map(
18
+ (entry) => toNuxtSitemapEntry(entry, locale, routePolicy, cmsConfig.localeMap)
19
+ );
20
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 预览模式中间件:token 穿透 + SWR 缓存绕过。
3
+ *
4
+ * 1. Token 穿透:将预览 cookie 存入 event.context,穿越 SWR 合成事件边界。
5
+ * SWR cachedEventHandler 的合成事件会剥离 cookie,但 event.context
6
+ * 通过引用共享(synthetic.context = original.context)可以穿越。
7
+ *
8
+ * 2. 缓存绕过:Nitro SWR cache key 基于 hash(event.node.req.url)(含 query)。
9
+ * 对页面路由追加唯一时间戳参数,使每次预览请求产生不同 cache key,
10
+ * 确保编辑后刷新总是获取最新草稿内容。
11
+ */
12
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, void>;
13
+ export default _default;
14
+ //# sourceMappingURL=preview-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-context.d.ts","sourceRoot":"","sources":["../../../../src/runtime/server/middleware/preview-context.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;;AACH,wBAsBE"}
@@ -0,0 +1,4 @@
1
+ /** CMS 重定向中间件 */
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
3
+ export default _default;
4
+ //# sourceMappingURL=redirects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirects.d.ts","sourceRoot":"","sources":["../../../../src/runtime/server/middleware/redirects.ts"],"names":[],"mappings":"AAsDA,iBAAiB;;AACjB,wBAiBE"}
@@ -3,6 +3,8 @@ import { defineEventHandler, getRequestURL, sendRedirect } from "h3";
3
3
  import { toArray } from "../../utils/list.js";
4
4
  let cachedRules = null;
5
5
  let cacheExpiry = 0;
6
+ const regexCache = /* @__PURE__ */ new Map();
7
+ const skippedPrefixes = ["/_", "/__", "/_nuxt", "/_ipx", "/.well-known", "/api"];
6
8
  async function loadRules() {
7
9
  const now = Date.now();
8
10
  if (cachedRules && now < cacheExpiry)
@@ -13,27 +15,42 @@ async function loadRules() {
13
15
  const { useServerCmsClient } = await import("../utils/cms-client.js");
14
16
  const client = useServerCmsClient();
15
17
  cachedRules = toArray(await client.getRedirects());
18
+ regexCache.clear();
16
19
  cacheExpiry = now + cacheTtl;
17
20
  return cachedRules;
18
- } catch (error) {
19
- console.error("[cms-nuxt] \u52A0\u8F7D\u91CD\u5B9A\u5411\u89C4\u5219\u5931\u8D25", error);
21
+ } catch {
20
22
  return cachedRules ?? [];
21
23
  }
22
24
  }
25
+ function matchesRule(rule, path) {
26
+ if (rule.match_type === "prefix")
27
+ return path.startsWith(rule.source_path);
28
+ if (rule.match_type === "regex") {
29
+ if (path.length > 2048)
30
+ return false;
31
+ let expression = regexCache.get(rule.source_path);
32
+ if (!expression) {
33
+ try {
34
+ expression = new RegExp(rule.source_path);
35
+ } catch {
36
+ return false;
37
+ }
38
+ regexCache.set(rule.source_path, expression);
39
+ }
40
+ return expression.test(path);
41
+ }
42
+ return path === rule.source_path;
43
+ }
23
44
  export default defineEventHandler(async (event) => {
24
45
  const path = getRequestURL(event).pathname;
25
- if (path.startsWith("/api/") || path.startsWith("/_nuxt/") || path.startsWith("/__nuxt")) {
46
+ const config = useRuntimeConfig();
47
+ const proxyPath = config.public.cms.content?.cmsProxyPath ?? "/cms-proxy";
48
+ if (path.includes(".") || [...skippedPrefixes, proxyPath].some((prefix) => path.startsWith(prefix))) {
26
49
  return;
27
50
  }
28
51
  const rules = await loadRules();
29
52
  for (const rule of rules) {
30
- let matched = false;
31
- if (rule.match_type === "exact" && path === rule.source_path) {
32
- matched = true;
33
- } else if (rule.match_type === "prefix" && path.startsWith(rule.source_path)) {
34
- matched = true;
35
- }
36
- if (matched) {
53
+ if (matchesRule(rule, path)) {
37
54
  return sendRedirect(event, rule.destination_path, rule.status_code);
38
55
  }
39
56
  }
@@ -0,0 +1,3 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<unknown>>;
2
+ export default _default;
3
+ //# sourceMappingURL=cms-proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cms-proxy.d.ts","sourceRoot":"","sources":["../../../../src/runtime/server/routes/cms-proxy.ts"],"names":[],"mappings":";AA0BA,wBA0CE"}
@@ -0,0 +1,55 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import {
3
+ createError,
4
+ defineEventHandler,
5
+ getCookie,
6
+ getQuery,
7
+ getRequestHeader,
8
+ getRequestURL,
9
+ setResponseHeader
10
+ } from "h3";
11
+ function queryString(query) {
12
+ const params = new URLSearchParams();
13
+ for (const [key, value] of Object.entries(query)) {
14
+ if (value !== void 0 && value !== null) {
15
+ params.set(key, String(value));
16
+ }
17
+ }
18
+ return params.toString();
19
+ }
20
+ function proxyPathname(pathname, proxyPath) {
21
+ const prefix = proxyPath.replace(/\/+$/g, "");
22
+ return decodeURIComponent(pathname.slice(prefix.length).replace(/^\/+/g, ""));
23
+ }
24
+ export default defineEventHandler(async (event) => {
25
+ setResponseHeader(event, "Cache-Control", "private, no-cache, no-store");
26
+ const config = useRuntimeConfig();
27
+ const cmsConfig = config.public.cms;
28
+ if (!cmsConfig.apiUrl) {
29
+ throw createError({ statusCode: 500, message: "CMS API URL not configured" });
30
+ }
31
+ const url = getRequestURL(event);
32
+ const proxyPath = cmsConfig.content?.cmsProxyPath ?? "/cms-proxy";
33
+ const path = proxyPathname(url.pathname, proxyPath);
34
+ const search = queryString(getQuery(event));
35
+ const targetUrl = `${cmsConfig.apiUrl.replace(/\/+$/g, "")}/${path}${search ? `?${search}` : ""}`;
36
+ const previewCookieName = cmsConfig.preview?.cookieName ?? "__preview_token";
37
+ const previewToken = getRequestHeader(event, "x-preview-token") || getCookie(event, previewCookieName);
38
+ const headers = { Accept: "application/json" };
39
+ if (cmsConfig.productCode)
40
+ headers["X-Product-Code"] = cmsConfig.productCode;
41
+ if (previewToken)
42
+ headers["X-Preview-Token"] = decodeURIComponent(previewToken);
43
+ const upstreamResponse = await $fetch.raw(targetUrl, {
44
+ headers,
45
+ ignoreResponseError: true
46
+ });
47
+ if (!upstreamResponse.ok) {
48
+ throw createError({
49
+ statusCode: upstreamResponse.status,
50
+ statusMessage: upstreamResponse.statusText,
51
+ data: upstreamResponse._data
52
+ });
53
+ }
54
+ return upstreamResponse._data;
55
+ });
@@ -0,0 +1,4 @@
1
+ import { CmsClient } from '@vigilkids/cms-client';
2
+ export declare function createServerCmsClient(previewToken?: string): CmsClient;
3
+ export declare function useServerCmsClient(): CmsClient;
4
+ //# sourceMappingURL=cms-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cms-client.d.ts","sourceRoot":"","sources":["../../../../src/runtime/server/utils/cms-client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAgBjD,wBAAgB,qBAAqB,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAQtE;AAED,wBAAgB,kBAAkB,IAAI,SAAS,CAK9C"}
@@ -1,15 +1,22 @@
1
1
  import { useRuntimeConfig } from "#imports";
2
2
  import { CmsClient } from "@vigilkids/cms-client";
3
3
  let _client = null;
4
+ function publicCmsConfig() {
5
+ const config = useRuntimeConfig();
6
+ return config.public.cms;
7
+ }
8
+ export function createServerCmsClient(previewToken) {
9
+ const publicCms = publicCmsConfig();
10
+ return new CmsClient({
11
+ baseUrl: publicCms.apiUrl,
12
+ productCode: publicCms.productCode,
13
+ defaultLocale: publicCms.defaultLocale,
14
+ previewToken
15
+ });
16
+ }
4
17
  export function useServerCmsClient() {
5
18
  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
- });
19
+ _client = createServerCmsClient();
13
20
  }
14
21
  return _client;
15
22
  }
@@ -0,0 +1,17 @@
1
+ import type { CmsNuxtModuleOptions } from '../../types';
2
+ /** 为 @nuxtjs/sitemap 提供 CMS 内容 URL */
3
+ export declare function sitemapProvider(options: CmsNuxtModuleOptions): Promise<{
4
+ loc: string;
5
+ lastmod?: string;
6
+ changefreq?: string;
7
+ priority?: number;
8
+ alternatives?: Array<{
9
+ hreflang: string;
10
+ href: string;
11
+ }>;
12
+ images?: Array<{
13
+ loc: string;
14
+ title?: string;
15
+ }>;
16
+ }[]>;
17
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/runtime/sitemap/provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAMvD,sCAAsC;AACtC,wBAAsB,eAAe,CAAC,OAAO,EAAE,oBAAoB;SAc1D,MAAM;cACD,MAAM;iBACH,MAAM;eACR,MAAM;mBACF,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;aAC/C,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;KAWlD"}
@@ -1,5 +1,7 @@
1
1
  import { CmsClient } from "@vigilkids/cms-client";
2
2
  import { toArray } from "../utils/list.js";
3
+ import { resolveCmsRoutePolicy } from "../utils/routes.js";
4
+ import { cmsLocale, toNuxtSitemapEntry } from "../utils/sitemap.js";
3
5
  export async function sitemapProvider(options) {
4
6
  const client = new CmsClient({
5
7
  baseUrl: options.apiUrl,
@@ -7,20 +9,16 @@ export async function sitemapProvider(options) {
7
9
  defaultLocale: options.defaultLocale
8
10
  });
9
11
  const locales = options.sitemap?.locales ?? [options.defaultLocale ?? "en"];
12
+ const routePolicy = resolveCmsRoutePolicy({
13
+ defaultLocale: options.defaultLocale,
14
+ localePrefixes: options.localePrefixes,
15
+ routes: options.routes
16
+ });
10
17
  const entries = [];
11
18
  for (const locale of locales) {
12
- const sitemapEntries = await client.getSitemap(locale);
19
+ const sitemapEntries = await client.getSitemap(cmsLocale(options.localeMap, locale));
13
20
  for (const entry of toArray(sitemapEntries)) {
14
- entries.push({
15
- loc: entry.loc,
16
- lastmod: entry.last_mod,
17
- changefreq: entry.change_freq,
18
- priority: entry.priority,
19
- alternatives: entry.hreflang?.map((h) => ({
20
- hreflang: h.lang,
21
- href: h.href
22
- }))
23
- });
21
+ entries.push(toNuxtSitemapEntry(entry, locale, routePolicy, options.localeMap));
24
22
  }
25
23
  }
26
24
  return entries;
@@ -0,0 +1,2 @@
1
+ export declare function toArray<T>(value: unknown): T[];
2
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/runtime/utils/list.ts"],"names":[],"mappings":"AAAA,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,CAAC,EAAE,CAE9C"}
@@ -0,0 +1,19 @@
1
+ import type { CmsRouteParams, CmsRoutePolicy, CmsRouteResources } from '../../types';
2
+ export declare const DEFAULT_CMS_ROUTE_RESOURCES: CmsRouteResources & Required<Pick<CmsRouteResources, 'article' | 'author' | 'category' | 'index'>>;
3
+ interface CmsRouteRuntimeConfig {
4
+ defaultLocale?: string;
5
+ localePrefixes?: Record<string, string>;
6
+ routes?: CmsRoutePolicy;
7
+ }
8
+ interface BuildCmsPathOptions {
9
+ includeLocale?: boolean;
10
+ }
11
+ export declare function resolveCmsRoutePolicy(config?: CmsRouteRuntimeConfig): Required<CmsRoutePolicy>;
12
+ export declare function buildCmsPath(policy: CmsRoutePolicy, resource: string, params?: CmsRouteParams, options?: BuildCmsPathOptions): string;
13
+ export declare function extractCmsRouteParams(pattern: string, path: string): CmsRouteParams | null;
14
+ export declare function extractCmsSourceRouteParams(policy: CmsRoutePolicy, resource: string, path: string): CmsRouteParams | null;
15
+ export declare function mergeCmsRouteParams(left: CmsRouteParams | null | undefined, right: CmsRouteParams | null | undefined): {
16
+ [x: string]: string | number | boolean | null | undefined;
17
+ };
18
+ export {};
19
+ //# sourceMappingURL=routes.d.ts.map