@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.
- package/CHANGELOG.md +111 -0
- package/README.md +239 -22
- package/dist/module.d.mts +37 -2
- package/dist/module.d.ts +5 -44
- package/dist/module.d.ts.map +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +91 -5
- package/dist/runtime/composables/useArticle.d.ts +16 -0
- package/dist/runtime/composables/useArticle.d.ts.map +1 -0
- package/dist/runtime/composables/useArticleList.d.ts +11 -0
- package/dist/runtime/composables/useArticleList.d.ts.map +1 -0
- package/dist/runtime/composables/useAuthor.d.ts +10 -0
- package/dist/runtime/composables/useAuthor.d.ts.map +1 -0
- package/dist/runtime/composables/useBlogSeo.d.ts +25 -0
- package/dist/runtime/composables/useBlogSeo.d.ts.map +1 -0
- package/dist/runtime/composables/useBlogSeo.js +99 -13
- package/dist/runtime/composables/useCategories.d.ts +10 -0
- package/dist/runtime/composables/useCategories.d.ts.map +1 -0
- package/dist/runtime/composables/useCmsArticleCollection.d.ts +14 -0
- package/dist/runtime/composables/useCmsArticleCollection.d.ts.map +1 -0
- package/dist/runtime/composables/useCmsArticleCollection.js +16 -0
- package/dist/runtime/composables/useCmsCategories.d.ts +188 -0
- package/dist/runtime/composables/useCmsCategories.d.ts.map +1 -0
- package/dist/runtime/composables/useCmsCategories.js +71 -0
- package/dist/runtime/composables/useCmsClient.d.ts +4 -0
- package/dist/runtime/composables/useCmsClient.d.ts.map +1 -0
- package/dist/runtime/composables/useCmsFetch.d.ts +19 -0
- package/dist/runtime/composables/useCmsFetch.d.ts.map +1 -0
- package/dist/runtime/composables/useCmsFetch.js +20 -0
- package/dist/runtime/composables/useCmsPreview.d.ts +16 -0
- package/dist/runtime/composables/useCmsPreview.d.ts.map +1 -0
- package/dist/runtime/composables/useCmsRoutes.d.ts +7 -0
- package/dist/runtime/composables/useCmsRoutes.d.ts.map +1 -0
- package/dist/runtime/composables/useCmsRoutes.js +10 -0
- package/dist/runtime/composables/useHreflangLinks.d.ts +34 -0
- package/dist/runtime/composables/useHreflangLinks.d.ts.map +1 -0
- package/dist/runtime/composables/useHreflangLinks.js +2 -1
- package/dist/runtime/plugins/preview-init.client.d.ts +4 -0
- package/dist/runtime/plugins/preview-init.client.d.ts.map +1 -0
- package/dist/runtime/server/api/_revalidate.post.d.ts +9 -0
- package/dist/runtime/server/api/_revalidate.post.d.ts.map +1 -0
- package/dist/runtime/server/api/_revalidate.post.js +30 -10
- package/dist/runtime/server/api/articles.get.d.ts +4 -0
- package/dist/runtime/server/api/articles.get.d.ts.map +1 -0
- package/dist/runtime/server/api/articles.get.js +92 -0
- package/dist/runtime/server/api/preview.get.d.ts +4 -0
- package/dist/runtime/server/api/preview.get.d.ts.map +1 -0
- package/dist/runtime/server/api/preview.get.js +47 -12
- package/dist/runtime/server/api/sitemap-urls.get.d.ts +16 -0
- package/dist/runtime/server/api/sitemap-urls.get.d.ts.map +1 -0
- package/dist/runtime/server/api/sitemap-urls.get.js +20 -0
- package/dist/runtime/server/middleware/preview-context.d.ts +14 -0
- package/dist/runtime/server/middleware/preview-context.d.ts.map +1 -0
- package/dist/runtime/server/middleware/redirects.d.ts +4 -0
- package/dist/runtime/server/middleware/redirects.d.ts.map +1 -0
- package/dist/runtime/server/middleware/redirects.js +27 -10
- package/dist/runtime/server/routes/cms-proxy.d.ts +3 -0
- package/dist/runtime/server/routes/cms-proxy.d.ts.map +1 -0
- package/dist/runtime/server/routes/cms-proxy.js +55 -0
- package/dist/runtime/server/utils/cms-client.d.ts +4 -0
- package/dist/runtime/server/utils/cms-client.d.ts.map +1 -0
- package/dist/runtime/server/utils/cms-client.js +14 -7
- package/dist/runtime/sitemap/provider.d.ts +17 -0
- package/dist/runtime/sitemap/provider.d.ts.map +1 -0
- package/dist/runtime/sitemap/provider.js +9 -11
- package/dist/runtime/utils/list.d.ts +2 -0
- package/dist/runtime/utils/list.d.ts.map +1 -0
- package/dist/runtime/utils/routes.d.ts +19 -0
- package/dist/runtime/utils/routes.d.ts.map +1 -0
- package/dist/runtime/utils/routes.js +119 -0
- package/dist/runtime/utils/sitemap.d.ts +20 -0
- package/dist/runtime/utils/sitemap.d.ts.map +1 -0
- package/dist/runtime/utils/sitemap.js +44 -0
- package/dist/types.d.ts +76 -7
- package/dist/types.d.ts.map +1 -0
- 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)
|
|
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 @@
|
|
|
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
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
paths
|
|
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 @@
|
|
|
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
|
|
8
|
-
const
|
|
9
|
-
if (!token || !
|
|
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
|
|
24
|
-
let
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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 @@
|
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 @@
|
|
|
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 @@
|
|
|
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
|
-
|
|
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 @@
|
|
|
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
|