@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.
Files changed (130) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +11 -0
  3. package/dist/config.client-B8As5DY0.js +2 -0
  4. package/dist/config.edge-B8As5DY0.js +2 -0
  5. package/dist/config.server-B8As5DY0.js +2 -0
  6. package/dist/exports/client-xcFJNcG7.d.ts +979 -0
  7. package/dist/exports/client-xcFJNcG7.d.ts.map +1 -0
  8. package/dist/exports/client.js +5735 -0
  9. package/dist/exports/client.js.map +1 -0
  10. package/dist/exports/instrumentation-DBO6QH4d.d.ts +13 -0
  11. package/dist/exports/instrumentation-DBO6QH4d.d.ts.map +1 -0
  12. package/dist/exports/instrumentation.js +25 -0
  13. package/dist/exports/instrumentation.js.map +1 -0
  14. package/dist/exports/proxy-Cv7QFVci.d.ts +71 -0
  15. package/dist/exports/proxy-Cv7QFVci.d.ts.map +1 -0
  16. package/dist/exports/proxy.js +254 -0
  17. package/dist/exports/proxy.js.map +1 -0
  18. package/dist/exports/server-BHObJHJ6.d.ts +402 -0
  19. package/dist/exports/server-BHObJHJ6.d.ts.map +1 -0
  20. package/dist/exports/server.js +1974 -0
  21. package/dist/exports/server.js.map +1 -0
  22. package/dist/http-_2M-UFRx.js +61 -0
  23. package/dist/http-_2M-UFRx.js.map +1 -0
  24. package/dist/index-CdYcvudT.d.ts +4 -0
  25. package/dist/index-CtfAJ0y7.d.ts +499 -0
  26. package/dist/index-CtfAJ0y7.d.ts.map +1 -0
  27. package/dist/index.js +201 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/route-DzFwERQP.js +65 -0
  30. package/dist/route-DzFwERQP.js.map +1 -0
  31. package/dist/store-CJzqrHsl.js +73 -0
  32. package/dist/store-CJzqrHsl.js.map +1 -0
  33. package/dist/types-BatUTdbZ.d.ts +15 -0
  34. package/dist/types-BatUTdbZ.d.ts.map +1 -0
  35. package/dist/types-D29_snYY.d.ts +191 -0
  36. package/dist/types-D29_snYY.d.ts.map +1 -0
  37. package/dist/utils-Ca7xh5mm.js +78 -0
  38. package/dist/utils-Ca7xh5mm.js.map +1 -0
  39. package/dist/utils-YwteTbk7.js +23 -0
  40. package/dist/utils-YwteTbk7.js.map +1 -0
  41. package/package.json +156 -0
  42. package/src/client/client.tsx +18 -0
  43. package/src/client/extra.tsx +21 -0
  44. package/src/client/label.tsx +24 -0
  45. package/src/client/layout.tsx +22 -0
  46. package/src/client/local.collection.ts +33 -0
  47. package/src/client/metadata.tsx +217 -0
  48. package/src/client/page.tsx +22 -0
  49. package/src/client/useApi.test.tsx +203 -0
  50. package/src/client/useApi.ts +59 -0
  51. package/src/client/useCollectionApi.test.tsx +201 -0
  52. package/src/client/useCollectionApi.ts +66 -0
  53. package/src/client/useExtra.tsx +12 -0
  54. package/src/client/useHttp.ts +54 -0
  55. package/src/client/useLabel.test.tsx +76 -0
  56. package/src/client/useLabel.tsx +12 -0
  57. package/src/client/useLayout.tsx +13 -0
  58. package/src/client/useLink.tsx +13 -0
  59. package/src/client/useMetaData.tsx +12 -0
  60. package/src/client/usePage.tsx +12 -0
  61. package/src/client/useUser.tsx +12 -0
  62. package/src/client/user.tsx +43 -0
  63. package/src/exports/client.ts +20 -0
  64. package/src/exports/instrumentation.ts +1 -0
  65. package/src/exports/proxy.ts +5 -0
  66. package/src/exports/server.ts +19 -0
  67. package/src/global.d.ts +6 -0
  68. package/src/index.ts +17 -0
  69. package/src/instrumentation/client.ts +19 -0
  70. package/src/instrumentation/sentry/capture.requestError.ts +6 -0
  71. package/src/instrumentation/sentry/capture.routerTransitionStart.ts +4 -0
  72. package/src/instrumentation/sentry/config.client.ts +9 -0
  73. package/src/instrumentation/sentry/config.edge.ts +9 -0
  74. package/src/instrumentation/sentry/config.server.ts +9 -0
  75. package/src/instrumentation/server.ts +24 -0
  76. package/src/instrumentation/types.ts +12 -0
  77. package/src/lib/breadcrumb.ts +21 -0
  78. package/src/lib/const.ts +3 -0
  79. package/src/lib/http.test.ts +25 -0
  80. package/src/lib/http.ts +74 -0
  81. package/src/lib/label.ts +43 -0
  82. package/src/lib/layout.defaults.ts +167 -0
  83. package/src/lib/link.ts +19 -0
  84. package/src/lib/meta-data.ts +60 -0
  85. package/src/lib/open-graph.ts +165 -0
  86. package/src/lib/pagination.ts +78 -0
  87. package/src/lib/payload.config.ts +12 -0
  88. package/src/lib/route.ts +83 -0
  89. package/src/lib/store.ts +63 -0
  90. package/src/lib/structured-data.ts +24 -0
  91. package/src/lib/twitter-card.ts +49 -0
  92. package/src/lib/utils.ts +45 -0
  93. package/src/mock.ts +264 -0
  94. package/src/proxy/protected-route.test.ts +75 -0
  95. package/src/proxy/protected-route.ts +57 -0
  96. package/src/proxy/proxy.test.ts +48 -0
  97. package/src/proxy/proxy.ts +64 -0
  98. package/src/proxy/route-auto-detection.ts +126 -0
  99. package/src/proxy/route-proxy.ts +187 -0
  100. package/src/server/context.ts +147 -0
  101. package/src/server/handlers/auth.handler.ts +27 -0
  102. package/src/server/handlers/captions.handler.ts +32 -0
  103. package/src/server/handlers/collection.handler.test.ts +57 -0
  104. package/src/server/handlers/collection.handler.ts +212 -0
  105. package/src/server/handlers/error-response.ts +45 -0
  106. package/src/server/handlers/index.handler.ts +146 -0
  107. package/src/server/handlers/preview.handler.ts +63 -0
  108. package/src/server/handlers/registry.handler.test.ts +65 -0
  109. package/src/server/handlers/registry.handler.ts +259 -0
  110. package/src/server/handlers/route.handler.ts +161 -0
  111. package/src/server/handlers/sitemap.handler.ts +374 -0
  112. package/src/server/handlers/static-params.handler.ts +111 -0
  113. package/src/server/handlers/vars.handler.ts +167 -0
  114. package/src/server/layout.ts +120 -0
  115. package/src/server/lazy.ts +41 -0
  116. package/src/server/local-api.ts +63 -0
  117. package/src/server/metadata.ts +87 -0
  118. package/src/server/mixer-payload.ts +124 -0
  119. package/src/server/mixer-payload.type-test.ts +27 -0
  120. package/src/server/mixer.ts +439 -0
  121. package/src/server/mock/category.ts +34 -0
  122. package/src/server/mock/collection.ts +21 -0
  123. package/src/server/mock/page.ts +62 -0
  124. package/src/server/mock/route.ts +36 -0
  125. package/src/server/mock/store.ts +159 -0
  126. package/src/server/page.ts +86 -0
  127. package/src/server/session/session.ts +101 -0
  128. package/src/server/utils.ts +23 -0
  129. package/src/server/utmz.ts +11 -0
  130. 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) &gt; 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) &lt; 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 & &amp;
324
+ Single Quote ' &apos;
325
+ Double Quote " &quot;
326
+ Greater Than > &gt;
327
+ Less Than < &lt;
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 = '&quot;';
345
+ break;
346
+ case 38: // &
347
+ escape = '&amp;';
348
+ break;
349
+ case 39: // '
350
+ escape = '&#39;';
351
+ break;
352
+ case 60: // <
353
+ escape = '&lt;';
354
+ break;
355
+ case 62: // >
356
+ escape = '&gt;';
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
+ */