@websolutespa/bom-next 3.0.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 +7 -0
- package/README.md +11 -0
- package/dist/config.client-B8As5DY0.js +2 -0
- package/dist/config.edge-B8As5DY0.js +2 -0
- package/dist/config.server-B8As5DY0.js +2 -0
- package/dist/exports/client-xcFJNcG7.d.ts +979 -0
- package/dist/exports/client-xcFJNcG7.d.ts.map +1 -0
- package/dist/exports/client.js +5735 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/instrumentation-DBO6QH4d.d.ts +13 -0
- package/dist/exports/instrumentation-DBO6QH4d.d.ts.map +1 -0
- package/dist/exports/instrumentation.js +25 -0
- package/dist/exports/instrumentation.js.map +1 -0
- package/dist/exports/proxy-Cv7QFVci.d.ts +71 -0
- package/dist/exports/proxy-Cv7QFVci.d.ts.map +1 -0
- package/dist/exports/proxy.js +254 -0
- package/dist/exports/proxy.js.map +1 -0
- package/dist/exports/server-BHObJHJ6.d.ts +402 -0
- package/dist/exports/server-BHObJHJ6.d.ts.map +1 -0
- package/dist/exports/server.js +1974 -0
- package/dist/exports/server.js.map +1 -0
- package/dist/http-_2M-UFRx.js +61 -0
- package/dist/http-_2M-UFRx.js.map +1 -0
- package/dist/index-CdYcvudT.d.ts +4 -0
- package/dist/index-CtfAJ0y7.d.ts +499 -0
- package/dist/index-CtfAJ0y7.d.ts.map +1 -0
- package/dist/index.js +201 -0
- package/dist/index.js.map +1 -0
- package/dist/route-DzFwERQP.js +65 -0
- package/dist/route-DzFwERQP.js.map +1 -0
- package/dist/store-CJzqrHsl.js +73 -0
- package/dist/store-CJzqrHsl.js.map +1 -0
- package/dist/types-BatUTdbZ.d.ts +15 -0
- package/dist/types-BatUTdbZ.d.ts.map +1 -0
- package/dist/types-D29_snYY.d.ts +191 -0
- package/dist/types-D29_snYY.d.ts.map +1 -0
- package/dist/utils-Ca7xh5mm.js +78 -0
- package/dist/utils-Ca7xh5mm.js.map +1 -0
- package/dist/utils-YwteTbk7.js +23 -0
- package/dist/utils-YwteTbk7.js.map +1 -0
- package/package.json +156 -0
- package/src/client/client.tsx +18 -0
- package/src/client/extra.tsx +21 -0
- package/src/client/label.tsx +24 -0
- package/src/client/layout.tsx +22 -0
- package/src/client/local.collection.ts +33 -0
- package/src/client/metadata.tsx +217 -0
- package/src/client/page.tsx +22 -0
- package/src/client/useApi.test.tsx +203 -0
- package/src/client/useApi.ts +59 -0
- package/src/client/useCollectionApi.test.tsx +201 -0
- package/src/client/useCollectionApi.ts +66 -0
- package/src/client/useExtra.tsx +12 -0
- package/src/client/useHttp.ts +54 -0
- package/src/client/useLabel.test.tsx +76 -0
- package/src/client/useLabel.tsx +12 -0
- package/src/client/useLayout.tsx +13 -0
- package/src/client/useLink.tsx +13 -0
- package/src/client/useMetaData.tsx +12 -0
- package/src/client/usePage.tsx +12 -0
- package/src/client/useUser.tsx +12 -0
- package/src/client/user.tsx +43 -0
- package/src/exports/client.ts +20 -0
- package/src/exports/instrumentation.ts +1 -0
- package/src/exports/proxy.ts +5 -0
- package/src/exports/server.ts +19 -0
- package/src/global.d.ts +6 -0
- package/src/index.ts +17 -0
- package/src/instrumentation/client.ts +19 -0
- package/src/instrumentation/sentry/capture.requestError.ts +6 -0
- package/src/instrumentation/sentry/capture.routerTransitionStart.ts +4 -0
- package/src/instrumentation/sentry/config.client.ts +9 -0
- package/src/instrumentation/sentry/config.edge.ts +9 -0
- package/src/instrumentation/sentry/config.server.ts +9 -0
- package/src/instrumentation/server.ts +24 -0
- package/src/instrumentation/types.ts +12 -0
- package/src/lib/breadcrumb.ts +21 -0
- package/src/lib/const.ts +3 -0
- package/src/lib/http.test.ts +25 -0
- package/src/lib/http.ts +74 -0
- package/src/lib/label.ts +43 -0
- package/src/lib/layout.defaults.ts +167 -0
- package/src/lib/link.ts +19 -0
- package/src/lib/meta-data.ts +60 -0
- package/src/lib/open-graph.ts +165 -0
- package/src/lib/pagination.ts +78 -0
- package/src/lib/payload.config.ts +12 -0
- package/src/lib/route.ts +83 -0
- package/src/lib/store.ts +63 -0
- package/src/lib/structured-data.ts +24 -0
- package/src/lib/twitter-card.ts +49 -0
- package/src/lib/utils.ts +45 -0
- package/src/mock.ts +264 -0
- package/src/proxy/protected-route.test.ts +75 -0
- package/src/proxy/protected-route.ts +57 -0
- package/src/proxy/proxy.test.ts +48 -0
- package/src/proxy/proxy.ts +64 -0
- package/src/proxy/route-auto-detection.ts +126 -0
- package/src/proxy/route-proxy.ts +187 -0
- package/src/server/context.ts +147 -0
- package/src/server/handlers/auth.handler.ts +27 -0
- package/src/server/handlers/captions.handler.ts +32 -0
- package/src/server/handlers/collection.handler.test.ts +57 -0
- package/src/server/handlers/collection.handler.ts +212 -0
- package/src/server/handlers/error-response.ts +45 -0
- package/src/server/handlers/index.handler.ts +146 -0
- package/src/server/handlers/preview.handler.ts +63 -0
- package/src/server/handlers/registry.handler.test.ts +65 -0
- package/src/server/handlers/registry.handler.ts +259 -0
- package/src/server/handlers/route.handler.ts +161 -0
- package/src/server/handlers/sitemap.handler.ts +374 -0
- package/src/server/handlers/static-params.handler.ts +111 -0
- package/src/server/handlers/vars.handler.ts +167 -0
- package/src/server/layout.ts +120 -0
- package/src/server/lazy.ts +41 -0
- package/src/server/local-api.ts +63 -0
- package/src/server/metadata.ts +87 -0
- package/src/server/mixer-payload.ts +124 -0
- package/src/server/mixer-payload.type-test.ts +27 -0
- package/src/server/mixer.ts +439 -0
- package/src/server/mock/category.ts +34 -0
- package/src/server/mock/collection.ts +21 -0
- package/src/server/mock/page.ts +62 -0
- package/src/server/mock/route.ts +36 -0
- package/src/server/mock/store.ts +159 -0
- package/src/server/page.ts +86 -0
- package/src/server/session/session.ts +101 -0
- package/src/server/utils.ts +23 -0
- package/src/server/utmz.ts +11 -0
- package/src/types.ts +211 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { IRedirect, IRoute, qsDeserialize } from '@websolutespa/bom-core';
|
|
2
|
+
import { getRoutes, getRouteSnapshot, sortCollection, whereCollection } from '@websolutespa/payload-plugin-bowl';
|
|
3
|
+
import { ResponseNotFound, ResponseSuccess } from '@websolutespa/payload-utils/server';
|
|
4
|
+
import { NextRequest } from 'next/server';
|
|
5
|
+
import { createPayloadRequest, getPayload as getPayload_, PayloadRequest, SanitizedConfig } from 'payload';
|
|
6
|
+
import { guest } from '../../lib/const';
|
|
7
|
+
import { sanitizeHref, sanitizeRedirectFrom, sanitizeRedirectTo, sanitizeRedirectUrl } from '../../lib/utils';
|
|
8
|
+
import { getHandlerErrorResponse } from './error-response';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates the internal `/-route` GET handler used by middleware to resolve CMS
|
|
12
|
+
* paths, route schemas, and redirects before a page is rendered.
|
|
13
|
+
*
|
|
14
|
+
* One Payload request is created per incoming API request so authentication and
|
|
15
|
+
* query context are shared by every route operation. The route service then
|
|
16
|
+
* reuses its snapshot cache, which avoids rebuilding the complete route graph
|
|
17
|
+
* for repeated market, locale, and access contexts.
|
|
18
|
+
*/
|
|
19
|
+
export function getRouteHandler(config: Promise<SanitizedConfig>) {
|
|
20
|
+
return async function (request: NextRequest) {
|
|
21
|
+
try {
|
|
22
|
+
const searchParams = request.nextUrl.searchParams;
|
|
23
|
+
const pathname = searchParams.get('pathname') || undefined;
|
|
24
|
+
const href = searchParams.get('href') || undefined;
|
|
25
|
+
const hrefBeforeRedirect = searchParams.get('hrefBeforeRedirect') || undefined;
|
|
26
|
+
const schema = searchParams.get('schema') || undefined;
|
|
27
|
+
const market = searchParams.get('market') || undefined;
|
|
28
|
+
const locale = searchParams.get('locale') || undefined;
|
|
29
|
+
const payload = await getPayload_({ config });
|
|
30
|
+
const req: PayloadRequest = await createPayloadRequest({
|
|
31
|
+
config,
|
|
32
|
+
request,
|
|
33
|
+
});
|
|
34
|
+
req.payload = payload;
|
|
35
|
+
req.user = guest as never;
|
|
36
|
+
if (schema && market && locale) {
|
|
37
|
+
// Schema lookups are used by middleware such as login protection. They
|
|
38
|
+
// resolve directly from the snapshot instead of enumerating routes.
|
|
39
|
+
req.query = { locale, market };
|
|
40
|
+
const route = (await getRouteSnapshot(req)).getSchema(schema, market, locale);
|
|
41
|
+
return route ? ResponseSuccess({ route }) : ResponseNotFound();
|
|
42
|
+
}
|
|
43
|
+
// console.log('routes.length', routes.length);
|
|
44
|
+
if (pathname && href) {
|
|
45
|
+
// Resolve the public path first. A successful result bypasses the
|
|
46
|
+
// redirect collection query, the costliest branch for normal traffic.
|
|
47
|
+
const resolution = (await getRouteSnapshot(req)).resolve(pathname);
|
|
48
|
+
/*
|
|
49
|
+
console.log('pathname', pathname);
|
|
50
|
+
console.log('href', href);
|
|
51
|
+
console.log('hrefBeforeRedirect', hrefBeforeRedirect);
|
|
52
|
+
*/
|
|
53
|
+
const route = resolution.route;
|
|
54
|
+
// console.log('route', route, 'pathname', pathname);
|
|
55
|
+
if (route !== undefined) {
|
|
56
|
+
return ResponseSuccess({
|
|
57
|
+
parent: resolution.parent,
|
|
58
|
+
route: {
|
|
59
|
+
...route,
|
|
60
|
+
...(resolution.splat ? { splat: resolution.splat } : {}),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
const url = new URL(href);
|
|
65
|
+
const urlBeforeRedirect = hrefBeforeRedirect ? new URL(hrefBeforeRedirect) : null;
|
|
66
|
+
const sanitizedHref = sanitizeHref(url, urlBeforeRedirect);
|
|
67
|
+
// Query only active redirects and request depth 0 because matching
|
|
68
|
+
// needs scalar fields only; this minimizes Payload serialization.
|
|
69
|
+
const { payload } = req;
|
|
70
|
+
const payloadResponse = await payload.find({
|
|
71
|
+
collection: 'redirect',
|
|
72
|
+
where: {
|
|
73
|
+
isActive: {
|
|
74
|
+
equals: true,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
sort: 'order',
|
|
78
|
+
depth: 0,
|
|
79
|
+
limit: 10000,
|
|
80
|
+
pagination: false,
|
|
81
|
+
req,
|
|
82
|
+
overrideAccess: true,
|
|
83
|
+
});
|
|
84
|
+
let redirects = (payloadResponse.docs ? payloadResponse.docs : []) as unknown as IRedirect[];
|
|
85
|
+
redirects = redirects.map(x => ({
|
|
86
|
+
...x,
|
|
87
|
+
from: sanitizeRedirectFrom(x.from, url, urlBeforeRedirect),
|
|
88
|
+
to: sanitizeRedirectTo(x.to),
|
|
89
|
+
}));
|
|
90
|
+
// redirects.sort((a, b) => a.order - b.order);
|
|
91
|
+
// exact match redirect resolver
|
|
92
|
+
const exactMatchResolver = (redirect: IRedirect) => {
|
|
93
|
+
const status: number = parseInt(redirect.status) || 307;
|
|
94
|
+
if (status < 400) {
|
|
95
|
+
const redirectUrl = sanitizeRedirectUrl(redirect.to, url);
|
|
96
|
+
// console.log('>>>>>>>>>> findRoute exactMatchResolver', status, redirectUrl, redirect.to);
|
|
97
|
+
return Response.json({ redirectUrl }, { status });
|
|
98
|
+
} else {
|
|
99
|
+
return Response.json('gone', { status });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
// check exact match redirects
|
|
103
|
+
const exactMatchRedirect = redirects.find(x => x.from === sanitizedHref);
|
|
104
|
+
if (exactMatchRedirect !== undefined) {
|
|
105
|
+
// Exact matching avoids compiling patterns for the common case.
|
|
106
|
+
return exactMatchResolver(exactMatchRedirect);
|
|
107
|
+
}
|
|
108
|
+
// pattern match redirect resolver
|
|
109
|
+
const patternMatchResolver = (redirect: IRedirect) => {
|
|
110
|
+
const status: number = parseInt(redirect.status) || 307;
|
|
111
|
+
if (status < 400) {
|
|
112
|
+
const regexp = new RegExp(redirect.from);
|
|
113
|
+
let i = 0;
|
|
114
|
+
let redirectUrl = (redirect.to as string).replace(/\*/g, (...rest) => {
|
|
115
|
+
return `$${++i}`;
|
|
116
|
+
});
|
|
117
|
+
redirectUrl = sanitizedHref.replace(regexp, redirectUrl);
|
|
118
|
+
// console.log('>>>>>>>>>> findRoute patternMatchResolver', status, redirectUrl, redirect.to);
|
|
119
|
+
return Response.json({ redirectUrl }, { status });
|
|
120
|
+
} else {
|
|
121
|
+
return Response.json('gone', { status });
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
// check redirects by pattern
|
|
125
|
+
redirects.forEach(x => {
|
|
126
|
+
let expression = x.from;
|
|
127
|
+
// wrapping catchall in (.*)
|
|
128
|
+
expression = expression.replace(/\*(?!\))/g, '(.*)');
|
|
129
|
+
// wrapping expression in ^...$
|
|
130
|
+
expression = `^${expression.replace(/(^\^)|(\$$)/g, '')}$`;
|
|
131
|
+
x.from = expression;
|
|
132
|
+
});
|
|
133
|
+
// check if a regex pattern redirect does match
|
|
134
|
+
const firstMatch = redirects.find(x => sanitizedHref.match(x.from));
|
|
135
|
+
// console.log('>>>>>>>>>> findRoute firstMatch', firstMatch);
|
|
136
|
+
if (firstMatch) {
|
|
137
|
+
return patternMatchResolver(firstMatch);
|
|
138
|
+
}
|
|
139
|
+
// console.error('not found', collection);
|
|
140
|
+
// Preserve the nearest parent so middleware can render a localized
|
|
141
|
+
// 404 without a second snapshot lookup.
|
|
142
|
+
return ResponseSuccess({ parent: resolution.parent });
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
// The collection branch serves management/list callers. Apply in-memory
|
|
146
|
+
// filtering after the cached route graph is built, avoiding duplicate
|
|
147
|
+
// collection queries for each filter or sort combination.
|
|
148
|
+
const query = qsDeserialize(decodeURIComponent(searchParams.toString()));
|
|
149
|
+
req.query = query;
|
|
150
|
+
const routes = await getRoutes(req);
|
|
151
|
+
let items = await whereCollection<IRoute>(routes, query.where);
|
|
152
|
+
items = await sortCollection(items, query.sort);
|
|
153
|
+
return ResponseSuccess(items);
|
|
154
|
+
// return ResponseBadRequest();
|
|
155
|
+
}
|
|
156
|
+
} catch (error: unknown) {
|
|
157
|
+
console.error('routeGetHandler.error', error);
|
|
158
|
+
return getHandlerErrorResponse(error);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { IRoute, IRouteCanonical, defaultLocale, defaultMarket, eachMarketLocale } from '@websolutespa/bom-core';
|
|
2
|
+
import { NextRequest } from 'next/server';
|
|
3
|
+
import { SanitizedConfig } from 'payload';
|
|
4
|
+
import { getPayload } from '../mixer';
|
|
5
|
+
import { getHandlerErrorResponse } from './error-response';
|
|
6
|
+
|
|
7
|
+
const origin = process.env.NEXT_PUBLIC_URL || '';
|
|
8
|
+
|
|
9
|
+
export type ISiteMap = {
|
|
10
|
+
id: string;
|
|
11
|
+
updatedAt?: string | Date;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function getSiteMapIndex(config: Promise<SanitizedConfig>) {
|
|
15
|
+
return async function (request: NextRequest) {
|
|
16
|
+
try {
|
|
17
|
+
const { getContext } = await getPayload({ config });
|
|
18
|
+
const { markets, locales, routes } = await getContext(defaultMarket, defaultLocale);
|
|
19
|
+
const sitemaps: ISiteMap[] = [];
|
|
20
|
+
const getTime = (date: Date | string | undefined) => {
|
|
21
|
+
return typeof date !== 'undefined' ? (typeof date === 'string' ? new Date(date) : date).getTime() : 0;
|
|
22
|
+
};
|
|
23
|
+
eachMarketLocale(markets, locales, (market, locale, markets, locales) => {
|
|
24
|
+
const sitemapRoutes = routes.filter(x => x.market === market.id && x.locale === locale.id);
|
|
25
|
+
sitemapRoutes.sort((a, b) => getTime(a.updatedAt) - getTime(b.updatedAt));
|
|
26
|
+
// console.log(sitemapRoutes.shift(), sitemapRoutes.pop());
|
|
27
|
+
sitemaps.push({
|
|
28
|
+
id: `/${market.id}/${locale.id}/sitemap.xml`,
|
|
29
|
+
updatedAt: sitemapRoutes.length > 0 ? sitemapRoutes[sitemapRoutes.length - 1]!.updatedAt : undefined,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="${origin}/sitemap.xsl"?>
|
|
33
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
34
|
+
${sitemaps.map(sitemap => `
|
|
35
|
+
<sitemap>
|
|
36
|
+
<loc>${escapeHtml(encodeURI(`${origin}${sitemap.id}`))}</loc>${sitemap.updatedAt ? `
|
|
37
|
+
<lastmod>${escapeHtml(String(sitemap.updatedAt))}</lastmod>` : ''}
|
|
38
|
+
</sitemap>
|
|
39
|
+
`).join('')}
|
|
40
|
+
</sitemapindex>`;
|
|
41
|
+
return new Response(body, {
|
|
42
|
+
status: 200,
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/xml; charset=UTF-8',
|
|
45
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
|
46
|
+
'X-Robots-Tag': 'noindex, follow',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
} catch (error: unknown) {
|
|
50
|
+
const response = getHandlerErrorResponse(error);
|
|
51
|
+
console.error('storeHandler.sitemapIndex.error', response.status, error);
|
|
52
|
+
return response;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getSiteMapXML(config: Promise<SanitizedConfig>) {
|
|
58
|
+
return async function (request: NextRequest, {
|
|
59
|
+
params: getParams,
|
|
60
|
+
}: {
|
|
61
|
+
params: Promise<{ market?: string; locale?: string; }>;
|
|
62
|
+
}) {
|
|
63
|
+
try {
|
|
64
|
+
const params = await getParams;
|
|
65
|
+
const market = params?.market || defaultMarket;
|
|
66
|
+
const locale = params?.locale || defaultLocale;
|
|
67
|
+
const { getContext } = await getPayload({ config });
|
|
68
|
+
const { markets, locales, routes } = await getContext(market, locale);
|
|
69
|
+
const selectedMarket = markets.find(x => market ? x.id === market : x.isDefault) || markets[0]!;
|
|
70
|
+
const selectedLocale = locales.find(x => locale ? x.id === locale : x.isDefault) || locales[0]!;
|
|
71
|
+
const selectedMarketLocale = selectedMarket && selectedMarket.defaultLanguage ? selectedMarket.defaultLanguage : selectedLocale.id;
|
|
72
|
+
const isCanonicalRoute: (route: IRoute) => boolean = (market && locale) ? (route) => {
|
|
73
|
+
return route.noindex !== true && route.market === selectedMarket.id && route.locale === selectedLocale.id;
|
|
74
|
+
} : (route) => {
|
|
75
|
+
return route.noindex !== true && route.market === selectedMarket.id && route.locale === selectedMarketLocale;
|
|
76
|
+
};
|
|
77
|
+
const getCanonicalAlternates: (canonical: IRoute) => IRoute[] = (market && locale) ? (canonical) => {
|
|
78
|
+
return [];
|
|
79
|
+
} : (canonical) => {
|
|
80
|
+
return routes.filter(alternate => {
|
|
81
|
+
return alternate !== canonical && alternate.schema === canonical.schema && alternate.page === canonical.page;
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
const routeToRouteCanonical: (canonical: IRoute) => IRouteCanonical = (canonical) => {
|
|
85
|
+
const alternates = getCanonicalAlternates(canonical);
|
|
86
|
+
return { ...canonical, alternates };
|
|
87
|
+
};
|
|
88
|
+
const canonicalRoutes: IRouteCanonical[] = routes.filter(isCanonicalRoute).map(routeToRouteCanonical);
|
|
89
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="${origin}/sitemap.xsl"?>
|
|
90
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
|
91
|
+
xmlns:xhtml="http://www.w3.org/1999/xhtml"
|
|
92
|
+
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
|
|
93
|
+
${getSiteMapXMLUrls(origin, canonicalRoutes)}
|
|
94
|
+
</urlset>
|
|
95
|
+
`;
|
|
96
|
+
return new Response(body, {
|
|
97
|
+
status: 200,
|
|
98
|
+
headers: {
|
|
99
|
+
'Content-Type': 'application/xml; charset=UTF-8',
|
|
100
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
|
101
|
+
'X-Robots-Tag': 'noindex, follow',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
} catch (error: unknown) {
|
|
105
|
+
const response = getHandlerErrorResponse(error);
|
|
106
|
+
console.error('storeHandler.sitemapXml.error', response.status, error);
|
|
107
|
+
return response;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getSiteMapXSL(config: Promise<SanitizedConfig>) {
|
|
113
|
+
return async function (request: NextRequest, {
|
|
114
|
+
params: getParams,
|
|
115
|
+
}: {
|
|
116
|
+
params: Promise<{ market?: string; locale?: string; }>;
|
|
117
|
+
}) {
|
|
118
|
+
try {
|
|
119
|
+
const params = await getParams;
|
|
120
|
+
const locale = params?.locale || defaultLocale;
|
|
121
|
+
const body = /* html */`<?xml version="1.0" encoding="UTF-8"?>
|
|
122
|
+
<xsl:stylesheet version="2.0"
|
|
123
|
+
xmlns:html="http://www.w3.org/TR/REC-html40"
|
|
124
|
+
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
|
|
125
|
+
xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9"
|
|
126
|
+
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
127
|
+
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
|
|
128
|
+
<xsl:template match="/">
|
|
129
|
+
<html lang="${locale}" xmlns="http://www.w3.org/1999/xhtml">
|
|
130
|
+
<head>
|
|
131
|
+
<title>XML Sitemap</title>
|
|
132
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
133
|
+
<style type="text/css">
|
|
134
|
+
html {
|
|
135
|
+
padding: 0;
|
|
136
|
+
background: #09090A;
|
|
137
|
+
color: #fefefe;
|
|
138
|
+
}
|
|
139
|
+
body {
|
|
140
|
+
padding: 15px;
|
|
141
|
+
font-family: Inter,-apple-system,'Helvetica Neue','Segoe UI',Roboto,Arial,'Noto Sans',sans-serif;
|
|
142
|
+
font-size: 13px;
|
|
143
|
+
background: #09090A;
|
|
144
|
+
color: #fefefe;
|
|
145
|
+
}
|
|
146
|
+
.container {
|
|
147
|
+
margin: 0 auto;
|
|
148
|
+
max-width: 1280px;
|
|
149
|
+
}
|
|
150
|
+
a {
|
|
151
|
+
color: #fefefe;
|
|
152
|
+
text-decoration: none;
|
|
153
|
+
}
|
|
154
|
+
a:visited {
|
|
155
|
+
color: #9e9e9e;
|
|
156
|
+
}
|
|
157
|
+
a:hover {
|
|
158
|
+
text-decoration: underline;
|
|
159
|
+
}
|
|
160
|
+
table {
|
|
161
|
+
width: 100%;
|
|
162
|
+
margin: 10px 0;
|
|
163
|
+
border: none;
|
|
164
|
+
border-collapse: collapse;
|
|
165
|
+
}
|
|
166
|
+
td,
|
|
167
|
+
th {
|
|
168
|
+
text-align: left;
|
|
169
|
+
font-size: 12px;
|
|
170
|
+
padding: 3px;
|
|
171
|
+
}
|
|
172
|
+
th:not(:last-child),
|
|
173
|
+
td:not(:last-child) {
|
|
174
|
+
padding-right: 30px;
|
|
175
|
+
}
|
|
176
|
+
thead th {
|
|
177
|
+
border-bottom: 1px solid #0e0e0f;
|
|
178
|
+
}
|
|
179
|
+
#sitemap tr:nth-child(odd) td {
|
|
180
|
+
background-color: #0e0e0f;
|
|
181
|
+
}
|
|
182
|
+
#sitemap tbody tr:hover td {
|
|
183
|
+
background-color: #1A1A1B;
|
|
184
|
+
}
|
|
185
|
+
#sitemap tbody tr:hover td,
|
|
186
|
+
#sitemap tbody tr:hover td a {
|
|
187
|
+
color: #fefefe;
|
|
188
|
+
}
|
|
189
|
+
.wysiwyg {
|
|
190
|
+
margin: 0;
|
|
191
|
+
padding: 0 0 10px 0;
|
|
192
|
+
line-height: 1.2em;
|
|
193
|
+
}
|
|
194
|
+
.wysiwyg a {
|
|
195
|
+
color: #00ea4e;
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
}
|
|
198
|
+
.wysiwyg a:visited {
|
|
199
|
+
color: #00ea4e;
|
|
200
|
+
}
|
|
201
|
+
</style>
|
|
202
|
+
</head>
|
|
203
|
+
<body>
|
|
204
|
+
<div class="container">
|
|
205
|
+
<h1>XML Sitemap</h1>
|
|
206
|
+
<p class="wysiwyg">
|
|
207
|
+
This is an XML Sitemap meant for consumption by search engines, generated by <a href="https://github.com/websolutespa/bom" target="_blank" rel="noopener">Mixer</a>.
|
|
208
|
+
</p>
|
|
209
|
+
<p class="wysiwyg">
|
|
210
|
+
You can find more information about XML sitemaps on <a href="https://sitemaps.org" target="_blank" rel="noopener">sitemaps.org</a>.
|
|
211
|
+
</p>
|
|
212
|
+
<xsl:if test="count(sitemap:sitemapindex/sitemap:sitemap) > 0">
|
|
213
|
+
<p class="wysiwyg">
|
|
214
|
+
This sitemap index file contains <xsl:value-of select="count(sitemap:sitemapindex/sitemap:sitemap)"/> sitemaps.
|
|
215
|
+
</p>
|
|
216
|
+
<table id="sitemap" cellpadding="3">
|
|
217
|
+
<thead>
|
|
218
|
+
<tr>
|
|
219
|
+
<th width="75%">Sitemap</th>
|
|
220
|
+
<th width="25%" title="Last Modification Time">Updated At</th>
|
|
221
|
+
</tr>
|
|
222
|
+
</thead>
|
|
223
|
+
<tbody>
|
|
224
|
+
<xsl:for-each select="sitemap:sitemapindex/sitemap:sitemap">
|
|
225
|
+
<xsl:variable name="sitemapURL">
|
|
226
|
+
<xsl:value-of select="sitemap:loc"/>
|
|
227
|
+
</xsl:variable>
|
|
228
|
+
<tr>
|
|
229
|
+
<td>
|
|
230
|
+
<a href="{$sitemapURL}"><xsl:value-of select="sitemap:loc"/></a>
|
|
231
|
+
</td>
|
|
232
|
+
<td>
|
|
233
|
+
<xsl:value-of select="concat(substring(sitemap:lastmod,0,11),concat(' ', substring(sitemap:lastmod,12,5)),concat(' ', substring(sitemap:lastmod,20,6)))"/>
|
|
234
|
+
</td>
|
|
235
|
+
</tr>
|
|
236
|
+
</xsl:for-each>
|
|
237
|
+
</tbody>
|
|
238
|
+
</table>
|
|
239
|
+
</xsl:if>
|
|
240
|
+
<xsl:if test="count(sitemap:sitemapindex/sitemap:sitemap) < 1">
|
|
241
|
+
<p class="wysiwyg">
|
|
242
|
+
This sitemap contains <xsl:value-of select="count(sitemap:urlset/sitemap:url)"/> URLs.
|
|
243
|
+
</p>
|
|
244
|
+
<table id="sitemap" cellpadding="3">
|
|
245
|
+
<thead>
|
|
246
|
+
<tr>
|
|
247
|
+
<th width="75%">URL</th>
|
|
248
|
+
<th width="15%" title="Last Modification Time">Updated At</th>
|
|
249
|
+
<th width="10%">Images</th>
|
|
250
|
+
</tr>
|
|
251
|
+
</thead>
|
|
252
|
+
<tbody>
|
|
253
|
+
<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
|
|
254
|
+
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
|
|
255
|
+
<xsl:for-each select="sitemap:urlset/sitemap:url">
|
|
256
|
+
<tr>
|
|
257
|
+
<td>
|
|
258
|
+
<xsl:variable name="itemURL">
|
|
259
|
+
<xsl:value-of select="sitemap:loc"/>
|
|
260
|
+
</xsl:variable>
|
|
261
|
+
<a href="{$itemURL}">
|
|
262
|
+
<xsl:value-of select="sitemap:loc"/>
|
|
263
|
+
</a>
|
|
264
|
+
</td>
|
|
265
|
+
<td>
|
|
266
|
+
<xsl:value-of select="concat(substring(sitemap:lastmod,0,11),concat(' ', substring(sitemap:lastmod,12,5)),concat(' ', substring(sitemap:lastmod,20,6)))"/>
|
|
267
|
+
</td>
|
|
268
|
+
<td>
|
|
269
|
+
<xsl:value-of select="count(image:image)"/>
|
|
270
|
+
</td>
|
|
271
|
+
</tr>
|
|
272
|
+
</xsl:for-each>
|
|
273
|
+
</tbody>
|
|
274
|
+
</table>
|
|
275
|
+
</xsl:if>
|
|
276
|
+
</div>
|
|
277
|
+
</body>
|
|
278
|
+
</html>
|
|
279
|
+
</xsl:template>
|
|
280
|
+
</xsl:stylesheet>`;
|
|
281
|
+
return new Response(body, {
|
|
282
|
+
status: 200,
|
|
283
|
+
headers: {
|
|
284
|
+
'Content-Type': 'application/xml; charset=UTF-8',
|
|
285
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
|
286
|
+
'X-Robots-Tag': 'noindex, follow',
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
} catch (error: unknown) {
|
|
290
|
+
const response = getHandlerErrorResponse(error);
|
|
291
|
+
console.error('storeHandler.sitemapXsl.error', response.status, error);
|
|
292
|
+
return response;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getSiteMapXMLLastMod(canonical: IRouteCanonical): string {
|
|
298
|
+
return canonical.updatedAt ? `
|
|
299
|
+
<lastmod>${escapeHtml(String(canonical.updatedAt))}</lastmod>` : '';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getSiteMapXMLImage(canonical: IRouteCanonical): string {
|
|
303
|
+
return canonical.media ? `
|
|
304
|
+
<image:image>
|
|
305
|
+
<image:loc>${escapeHtml(encodeURI(`${canonical.media.src || canonical.media.url}`))}</image:loc>
|
|
306
|
+
</image:image>` : '';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getSiteMapXMLAlternates(origin: string, canonical: IRouteCanonical): string {
|
|
310
|
+
return canonical.alternates.map(alternate => `
|
|
311
|
+
<xhtml:link href="${escapeHtml(encodeURI(`${origin}${alternate.id}`))}" hreflang="${alternate.locale}-${alternate.market}" rel="alternate" />`).join('');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function getSiteMapXMLUrl(origin: string, canonical: IRouteCanonical): string {
|
|
315
|
+
return `
|
|
316
|
+
<url>
|
|
317
|
+
<loc>${escapeHtml(encodeURI(`${origin}${canonical.id}`))}</loc>${getSiteMapXMLLastMod(canonical)}${getSiteMapXMLAlternates(origin, canonical)}${getSiteMapXMLImage(canonical)}
|
|
318
|
+
</url>`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/*
|
|
322
|
+
Character Escape Code
|
|
323
|
+
Ampersand & &
|
|
324
|
+
Single Quote ' '
|
|
325
|
+
Double Quote " "
|
|
326
|
+
Greater Than > >
|
|
327
|
+
Less Than < <
|
|
328
|
+
*/
|
|
329
|
+
|
|
330
|
+
function escapeHtml(string: string) {
|
|
331
|
+
const matchHtmlRegExp = /["'&<>]/;
|
|
332
|
+
const str = '' + string;
|
|
333
|
+
const match = matchHtmlRegExp.exec(str);
|
|
334
|
+
if (!match) {
|
|
335
|
+
return str;
|
|
336
|
+
}
|
|
337
|
+
let escape: string;
|
|
338
|
+
let html = '';
|
|
339
|
+
let index = 0;
|
|
340
|
+
let lastIndex = 0;
|
|
341
|
+
for (index = match.index; index < str.length; index++) {
|
|
342
|
+
switch (str.charCodeAt(index)) {
|
|
343
|
+
case 34: // "
|
|
344
|
+
escape = '"';
|
|
345
|
+
break;
|
|
346
|
+
case 38: // &
|
|
347
|
+
escape = '&';
|
|
348
|
+
break;
|
|
349
|
+
case 39: // '
|
|
350
|
+
escape = ''';
|
|
351
|
+
break;
|
|
352
|
+
case 60: // <
|
|
353
|
+
escape = '<';
|
|
354
|
+
break;
|
|
355
|
+
case 62: // >
|
|
356
|
+
escape = '>';
|
|
357
|
+
break;
|
|
358
|
+
default:
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (lastIndex !== index) {
|
|
362
|
+
html += str.substring(lastIndex, index);
|
|
363
|
+
}
|
|
364
|
+
lastIndex = index + 1;
|
|
365
|
+
html += escape;
|
|
366
|
+
}
|
|
367
|
+
return lastIndex !== index
|
|
368
|
+
? html + str.substring(lastIndex, index)
|
|
369
|
+
: html;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function getSiteMapXMLUrls(origin: string, canonicalRoutes: IRouteCanonical[]): string {
|
|
373
|
+
return canonicalRoutes.map((canonicalRoute) => getSiteMapXMLUrl(origin, canonicalRoute)).join('');
|
|
374
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { isDevelopment, qsSerialize, type IRoute } from '@websolutespa/bom-core';
|
|
2
|
+
import type { CollectionSlug, SanitizedConfig } from 'payload';
|
|
3
|
+
import { guest } from '../../lib/const';
|
|
4
|
+
import { localApiGet } from '../../lib/http';
|
|
5
|
+
import { BuildStrategy, buildStrategy } from '../../lib/store';
|
|
6
|
+
import { PageParams } from '../../types';
|
|
7
|
+
import { getPayload, getPayloadInstance, getRoutes } from '../mixer';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns a generateStaticParams function for a given page collection slug.
|
|
11
|
+
*/
|
|
12
|
+
export function getStaticParams<TSlug extends CollectionSlug>(slug: TSlug, config: Promise<SanitizedConfig>) {
|
|
13
|
+
return async function generateStaticParams(): Promise<PageParams[]> {
|
|
14
|
+
if (
|
|
15
|
+
isDevelopment ||
|
|
16
|
+
buildStrategy === BuildStrategy.Runtime
|
|
17
|
+
) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const { getContext } = await getPayload({ config });
|
|
21
|
+
const { routes } = await getContext('all', 'all');
|
|
22
|
+
return (routes)
|
|
23
|
+
.filter(r => r.schema === slug)
|
|
24
|
+
.map(route => ({
|
|
25
|
+
market: route.market,
|
|
26
|
+
locale: route.locale,
|
|
27
|
+
id: String(route.page),
|
|
28
|
+
}));
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// unused
|
|
33
|
+
export function getStaticParams_(schema: string) {
|
|
34
|
+
return async function generateStaticParams(): Promise<{
|
|
35
|
+
market: string;
|
|
36
|
+
locale: string;
|
|
37
|
+
id: string;
|
|
38
|
+
}[]> {
|
|
39
|
+
if (
|
|
40
|
+
isDevelopment ||
|
|
41
|
+
buildStrategy === BuildStrategy.Runtime
|
|
42
|
+
) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const query = qsSerialize({
|
|
46
|
+
where: {
|
|
47
|
+
or: [{
|
|
48
|
+
schema: {
|
|
49
|
+
equals: schema,
|
|
50
|
+
},
|
|
51
|
+
template: {
|
|
52
|
+
equals: schema,
|
|
53
|
+
},
|
|
54
|
+
}],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const routes: IRoute[] = await localApiGet(`/-route?${query}`);
|
|
58
|
+
return (routes)
|
|
59
|
+
.map(route => ({
|
|
60
|
+
market: route.market,
|
|
61
|
+
locale: route.locale,
|
|
62
|
+
id: String(route.page),
|
|
63
|
+
}));
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// unused
|
|
68
|
+
export function getStaticParamsFromPayload(schema: string, config: Promise<SanitizedConfig>) {
|
|
69
|
+
return async function generateStaticParams(): Promise<{
|
|
70
|
+
market: string;
|
|
71
|
+
locale: string;
|
|
72
|
+
id: string;
|
|
73
|
+
}[]> {
|
|
74
|
+
if (
|
|
75
|
+
isDevelopment ||
|
|
76
|
+
buildStrategy === BuildStrategy.Runtime
|
|
77
|
+
) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const payload = await getPayloadInstance({ config });
|
|
82
|
+
const routes = await getRoutes({
|
|
83
|
+
payload,
|
|
84
|
+
market: 'all',
|
|
85
|
+
locale: 'all',
|
|
86
|
+
user: guest,
|
|
87
|
+
draft: false,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return routes
|
|
91
|
+
.filter(route => (
|
|
92
|
+
route.schema === schema ||
|
|
93
|
+
route.template === schema
|
|
94
|
+
))
|
|
95
|
+
.map(route => ({
|
|
96
|
+
market: route.market,
|
|
97
|
+
locale: route.locale,
|
|
98
|
+
id: String(route.page),
|
|
99
|
+
}));
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
Good to know:
|
|
105
|
+
You can use the dynamicParams segment config option to control what happens when a dynamic segment is visited that was not generated with generateStaticParams.
|
|
106
|
+
You must return an empty array from generateStaticParams or utilize export const dynamic = 'force-static' in order to revalidate (ISR) paths at runtime.
|
|
107
|
+
During next dev, generateStaticParams will be called when you navigate to a route.
|
|
108
|
+
During next build, generateStaticParams runs before the corresponding Layouts or Pages are generated.
|
|
109
|
+
During revalidation (ISR), generateStaticParams will not be called again.
|
|
110
|
+
generateStaticParams replaces the getStaticPaths function in the Pages Router.
|
|
111
|
+
*/
|