create-ailk 0.26.0 → 0.28.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/README.md +86 -9
- package/dist/add-module-core.js +2 -1
- package/dist/add-module.d.ts +2 -1
- package/dist/add-module.js +9 -4
- package/dist/add-route.js +46 -19
- package/dist/body-heading.d.ts +56 -0
- package/dist/body-heading.js +394 -0
- package/dist/cli.js +28 -1
- package/dist/content-payloads.js +2 -2
- package/dist/derive-architecture.js +8 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/lib/auth-secret-file.d.ts +6 -2
- package/dist/lib/auth-secret-file.js +46 -20
- package/dist/lib/env-catalog.js +20 -0
- package/dist/lib/site-modules.d.ts +8 -0
- package/dist/lib/site-modules.js +34 -0
- package/dist/module-architecture.js +25 -14
- package/dist/paid-sections.d.ts +17 -0
- package/dist/paid-sections.js +39 -0
- package/dist/programmatic.d.ts +16 -0
- package/dist/programmatic.js +73 -4
- package/dist/remove-module.d.ts +98 -0
- package/dist/remove-module.js +620 -0
- package/dist/seed-flows.d.ts +2 -2
- package/dist/seed-flows.js +16 -9
- package/dist/site-roster.js +4 -1
- package/dist/starter-grid.d.ts +26 -0
- package/dist/starter-grid.js +34 -0
- package/dist/surfaces.d.ts +26 -0
- package/dist/surfaces.js +112 -11
- package/dist/sync-routes.d.ts +7 -0
- package/dist/sync-routes.js +53 -6
- package/dist/template-pages.d.ts +36 -0
- package/dist/template-pages.js +129 -0
- package/dist/topology.d.ts +2 -1
- package/dist/topology.js +4 -2
- package/dist/update.d.ts +3 -1
- package/dist/update.js +90 -6
- package/package.json +3 -3
- package/release-scope.json +2 -1
- package/site-templates/aliases/hub.TEMPLATE-MANIFEST.json +115 -0
- package/templates/.env.example +12 -2
- package/templates/CLAUDE.md +20 -20
- package/templates/README.md +19 -19
- package/templates/apps/api/.env.example +16 -5
- package/templates/apps/api/CLAUDE.md +32 -11
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +37 -0
- package/templates/apps/api/src/vercel-handler.ts +8 -1
- package/templates/apps/api/vercel.json +7 -1
- package/templates/apps/mcp/CLAUDE.md +18 -10
- package/templates/apps/web/.env.example +27 -0
- package/templates/apps/web/CLAUDE.md +56 -8
- package/templates/apps/web/__tests__/setup.ts +7 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +7 -7
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/next-path.test.ts +24 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +51 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +13 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +10 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/page.test.tsx +197 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +23 -7
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +61 -2
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/connect/__tests__/page.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/(authed)/account/connect/page.tsx +152 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +76 -21
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +46 -16
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +180 -2
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +122 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/hub-children.test.tsx +65 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +23 -11
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/consent.actions.ts +44 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +269 -42
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +88 -30
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/courses/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/courses/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/events/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/events/page.tsx +23 -7
- package/templates/apps/web/app/[locale]/features/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +23 -25
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +95 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/save.actions.test.ts +193 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/email-moment.ts +92 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +10 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +513 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +122 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.no-commerce.test.tsx +195 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/landing.test.tsx +293 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +397 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/checkout-seam.ts +74 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +264 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/save.actions.ts +75 -0
- package/templates/apps/web/app/[locale]/for/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +5 -1
- package/templates/apps/web/app/[locale]/for/page.tsx +23 -25
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +25 -4
- package/templates/apps/web/app/[locale]/integrations/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/integrations/page.tsx +23 -11
- package/templates/apps/web/app/[locale]/layout.tsx +161 -128
- package/templates/apps/web/app/[locale]/page.tsx +51 -3
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +16 -3
- package/templates/apps/web/app/[locale]/partners/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/partners/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/portfolio/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +25 -24
- package/templates/apps/web/app/[locale]/pricing/[slug]/__tests__/page.test.tsx +321 -0
- package/templates/apps/web/app/[locale]/pricing/[slug]/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/pricing-pages.test.tsx +215 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +56 -341
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +21 -4
- package/templates/apps/web/app/[locale]/products/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/products/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/projects/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +18 -11
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +25 -4
- package/templates/apps/web/app/[locale]/resources/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/resources/page.tsx +20 -10
- package/templates/apps/web/app/[locale]/services/[slug]/[child]/page.tsx +68 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +64 -9
- package/templates/apps/web/app/[locale]/services/__tests__/hub-children.test.tsx +158 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/services/__tests__/service-detail-sections.test.tsx +223 -0
- package/templates/apps/web/app/[locale]/services/__tests__/service-families.test.tsx +368 -0
- package/templates/apps/web/app/[locale]/services/page.tsx +45 -19
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/team/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/team/page.tsx +25 -24
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +24 -3
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/templates/[slug]/__tests__/page.test.tsx +203 -0
- package/templates/apps/web/app/[locale]/templates/[slug]/page.tsx +81 -0
- package/templates/apps/web/app/[locale]/templates/__tests__/page.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/templates/page.tsx +108 -0
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +24 -3
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +322 -2
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +161 -29
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +51 -0
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +13 -0
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +233 -7
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +92 -41
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +77 -39
- package/templates/apps/web/app/llms.txt/route.ts +41 -3
- package/templates/apps/web/app/robots.txt/route.ts +5 -1
- package/templates/apps/web/app/sitemap.xml/route.ts +45 -3
- package/templates/apps/web/auth.ts +102 -50
- package/templates/apps/web/components/signed-in-nav.tsx +8 -1
- package/templates/apps/web/lib/__tests__/bot-check.test.ts +254 -0
- package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +212 -13
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +13 -1
- package/templates/apps/web/lib/__tests__/docs-index.test.ts +93 -0
- package/templates/apps/web/lib/__tests__/docs-path.test.ts +168 -0
- package/templates/apps/web/lib/__tests__/hub-children.fixtures.ts +278 -0
- package/templates/apps/web/lib/__tests__/nav-label-keys.test.ts +138 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +178 -2
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +183 -0
- package/templates/apps/web/lib/__tests__/page-chrome.test.ts +412 -0
- package/templates/apps/web/lib/__tests__/pricing-page.test.ts +142 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +484 -4
- package/templates/apps/web/lib/__tests__/route-family.test.ts +57 -0
- package/templates/apps/web/lib/__tests__/service-listing.test.ts +111 -0
- package/templates/apps/web/lib/__tests__/services-path.test.ts +96 -0
- package/templates/apps/web/lib/__tests__/site-header.test.ts +151 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +148 -6
- package/templates/apps/web/lib/__tests__/telemetry-scrub.test.ts +84 -0
- package/templates/apps/web/lib/__tests__/template-listing.test.ts +158 -0
- package/templates/apps/web/lib/auth-secret.ts +57 -0
- package/templates/apps/web/lib/bot-check.ts +138 -0
- package/templates/apps/web/lib/consent-visitor.ts +81 -7
- package/templates/apps/web/lib/deck-page.ts +6 -3
- package/templates/apps/web/lib/docs-index.ts +100 -0
- package/templates/apps/web/lib/docs-path.ts +94 -0
- package/templates/apps/web/lib/hub-children.tsx +318 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +22 -2
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +11 -8
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/nav-label-check.ts +61 -0
- package/templates/apps/web/lib/nav-label-keys.ts +78 -0
- package/templates/apps/web/lib/newsletter.actions.ts +31 -6
- package/templates/apps/web/lib/offerings-pricing.ts +135 -6
- package/templates/apps/web/lib/page-chrome.ts +367 -0
- package/templates/apps/web/lib/posting-title.ts +26 -4
- package/templates/apps/web/lib/pricing-page.ts +430 -0
- package/templates/apps/web/lib/relay.ts +44 -4
- package/templates/apps/web/lib/route-family.ts +69 -0
- package/templates/apps/web/lib/service-listing.ts +89 -0
- package/templates/apps/web/lib/services-path.ts +97 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +17 -6
- package/templates/apps/web/lib/site-header.ts +94 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +68 -3
- package/templates/apps/web/lib/template-listing.ts +169 -0
- package/templates/apps/web/messages/en.json +144 -3
- package/templates/apps/web/middleware.ts +179 -12
- package/templates/apps/web/next.config.mjs +220 -3
- package/templates/apps/web/package.json +3 -2
- package/templates/apps/web/scripts/assert-content-bundled.ts +28 -6
- package/templates/apps/web/scripts/check-nav-labels.ts +39 -0
- package/templates/apps/web/sentry.server.config.ts +30 -2
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -9
- package/templates/apps/web/src/lib/rate-limit.ts +68 -27
- package/templates/apps/web/src/lib/routes.ts +15 -1
- package/versions.json +48 -48
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
// Only a buyer's page load starts anything: a prefetch, a crawler, a
|
|
7
7
|
// cross-site load or an ungrammatical key is refused before any call. The
|
|
8
8
|
// route then confirms, from a shown-set read shared for 15 seconds, that the
|
|
9
|
-
// site is connected and shows the key —
|
|
9
|
+
// site is connected and shows the key — and, for a bundle link, the required
|
|
10
|
+
// product's key it carries as `?with=` (#10028 D10), sent as the body's
|
|
11
|
+
// `requires` — an unconnected site's POST would take
|
|
10
12
|
// the API's env path (#8931 A9) — and POSTs `/v1/checkout/sessions` with
|
|
11
13
|
// success and cancel URLs on the site's configured origin, never the
|
|
12
14
|
// request's. Every refusal is a 303 to `/pricing` with no body and one fixed
|
|
@@ -19,6 +21,10 @@ import { isShownLookupKey } from "../../../../../lib/offerings-pricing";
|
|
|
19
21
|
import { FALLBACK_BASE_URL } from "../../../../../lib/site-brand";
|
|
20
22
|
import { TOPOLOGY } from "../../../../../lib/topology.generated";
|
|
21
23
|
import { resolveCommerceClaim } from "../../../../../src/lib/commerce-claim";
|
|
24
|
+
import {
|
|
25
|
+
clientIpFromHeaders,
|
|
26
|
+
UNKNOWN_VISITOR_FACTOR,
|
|
27
|
+
} from "../../../../../src/lib/rate-limit";
|
|
22
28
|
import { resolveBaseUrl } from "../../../../../src/lib/routes";
|
|
23
29
|
|
|
24
30
|
export const runtime = "nodejs";
|
|
@@ -166,37 +172,75 @@ function siteOrigin(): string | null {
|
|
|
166
172
|
const BUDGET_STARTS = 5;
|
|
167
173
|
const BUDGET_WINDOW_MS = 10 * 60_000;
|
|
168
174
|
const BUDGET_MAX_ADDRESSES = 10_000;
|
|
169
|
-
/** An address key is cut to this length, so the Map cannot grow by key size. */
|
|
170
|
-
const BUDGET_ADDRESS_LENGTH = 64;
|
|
171
175
|
|
|
172
|
-
|
|
176
|
+
/** One visitor's window: the starts it made, and the ones still in flight. */
|
|
177
|
+
interface StartWindow {
|
|
178
|
+
windowStart: number;
|
|
179
|
+
count: number;
|
|
180
|
+
pending: number;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const starts = new Map<string, StartWindow>();
|
|
184
|
+
/** The address-less visitors' one window, which no address key can evict. */
|
|
185
|
+
const unknownStarts: StartWindow = { windowStart: 0, count: 0, pending: 0 };
|
|
173
186
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
/** Starts `window` afresh once its time is up, in place, so a start in flight settles on it. */
|
|
188
|
+
function rollOver(window: StartWindow, now: number): void {
|
|
189
|
+
if (now - window.windowStart < BUDGET_WINDOW_MS) return;
|
|
190
|
+
window.windowStart = now;
|
|
191
|
+
window.count = 0;
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
195
|
+
* The window for `address`, re-inserted when it rolls over so the
|
|
196
|
+
* oldest-inserted window evicted at BUDGET_MAX_ADDRESSES is the stalest.
|
|
184
197
|
*/
|
|
185
|
-
function
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
function addressWindow(address: string, now: number): StartWindow {
|
|
199
|
+
const window = starts.get(address);
|
|
200
|
+
if (window) {
|
|
201
|
+
if (now - window.windowStart >= BUDGET_WINDOW_MS) {
|
|
202
|
+
rollOver(window, now);
|
|
203
|
+
starts.delete(address);
|
|
204
|
+
starts.set(address, window);
|
|
205
|
+
}
|
|
206
|
+
return window;
|
|
191
207
|
}
|
|
192
|
-
if (
|
|
193
|
-
starts.delete(address);
|
|
194
|
-
} else if (starts.size >= BUDGET_MAX_ADDRESSES) {
|
|
208
|
+
if (starts.size >= BUDGET_MAX_ADDRESSES) {
|
|
195
209
|
const oldest = starts.keys().next().value;
|
|
196
210
|
if (oldest !== undefined) starts.delete(oldest);
|
|
197
211
|
}
|
|
198
|
-
|
|
199
|
-
|
|
212
|
+
const fresh = { windowStart: now, count: 0, pending: 0 };
|
|
213
|
+
starts.set(address, fresh);
|
|
214
|
+
return fresh;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Reserves one start for the visitor, or answers null when its window is
|
|
219
|
+
* spent (#9059). The key is `clientIpFromHeaders`'s (#9967 D1): Vercel's own
|
|
220
|
+
* `x-vercel-forwarded-for`, read only on Vercel, never a header the client
|
|
221
|
+
* chooses. A visitor with no readable address shares one window
|
|
222
|
+
* UNKNOWN_VISITOR_FACTOR times as wide, never skipped. Starts in flight hold
|
|
223
|
+
* their place, so concurrent clicks cannot overrun the window.
|
|
224
|
+
*/
|
|
225
|
+
function reserveStart(headers: Headers, now: number): StartWindow | null {
|
|
226
|
+
const address = clientIpFromHeaders(headers);
|
|
227
|
+
let window = unknownStarts;
|
|
228
|
+
let limit = UNKNOWN_VISITOR_FACTOR * BUDGET_STARTS;
|
|
229
|
+
if (address === "unknown") {
|
|
230
|
+
rollOver(window, now);
|
|
231
|
+
} else {
|
|
232
|
+
window = addressWindow(address, now);
|
|
233
|
+
limit = BUDGET_STARTS;
|
|
234
|
+
}
|
|
235
|
+
if (window.count + window.pending >= limit) return null;
|
|
236
|
+
window.pending += 1;
|
|
237
|
+
return window;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Only a Checkout the API started spends the start (#9063). */
|
|
241
|
+
function settleStart(window: StartWindow, started: boolean): void {
|
|
242
|
+
window.pending -= 1;
|
|
243
|
+
if (started) window.count += 1;
|
|
200
244
|
}
|
|
201
245
|
|
|
202
246
|
// ─── D2 steps 8 and 9 — the start and its answer ────────────────────────────
|
|
@@ -263,9 +307,12 @@ async function startCheckout(
|
|
|
263
307
|
ctx: BuyContext,
|
|
264
308
|
): Promise<Response> {
|
|
265
309
|
const { lookupKey } = await ctx.params;
|
|
310
|
+
// #10028 D10 — a bundle link names the required product's key as `?with=`.
|
|
311
|
+
const withKey = new URL(request.url).searchParams.get("with");
|
|
266
312
|
|
|
267
313
|
if (!isBuyerNavigation(request)) return refuse("unintended");
|
|
268
314
|
if (!parseLookupKey(lookupKey)) return refuse("ungrammatical");
|
|
315
|
+
if (withKey !== null && !parseLookupKey(withKey)) return refuse("ungrammatical");
|
|
269
316
|
if (!(await resolveCommerceClaim())) return refuse("no_claim");
|
|
270
317
|
if (process.env.NODE_ENV === "production" && !configuredApiBase()) {
|
|
271
318
|
return refuse("unconnected");
|
|
@@ -276,29 +323,33 @@ async function startCheckout(
|
|
|
276
323
|
if (shown.kind !== "shown" || !isShownLookupKey(shown.shownSet, lookupKey)) {
|
|
277
324
|
return refuse("not_shown");
|
|
278
325
|
}
|
|
326
|
+
if (withKey !== null && !isShownLookupKey(shown.shownSet, withKey)) {
|
|
327
|
+
return refuse("not_shown");
|
|
328
|
+
}
|
|
279
329
|
|
|
280
330
|
const origin = siteOrigin();
|
|
281
331
|
if (!origin) return refuse("no_origin");
|
|
282
332
|
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
333
|
+
const headers = startHeaders(request.headers);
|
|
334
|
+
const reserved = reserveStart(request.headers, Date.now());
|
|
335
|
+
if (!reserved) return refuse("rate_limited");
|
|
336
|
+
|
|
337
|
+
// Settled on the API's own answer, not the timeout: a start that answers
|
|
338
|
+
// late still spends, and one in flight still holds its place.
|
|
339
|
+
const start = checkout
|
|
340
|
+
.createSession(
|
|
341
|
+
{
|
|
342
|
+
productCode: lookupKey,
|
|
343
|
+
...(withKey !== null && { requires: withKey }),
|
|
344
|
+
successUrl: `${origin}/purchase/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
345
|
+
cancelUrl: `${origin}/pricing`,
|
|
346
|
+
},
|
|
347
|
+
{ headers },
|
|
348
|
+
)
|
|
349
|
+
.catch(() => null);
|
|
350
|
+
void start.then((answer) => settleStart(reserved, answer?.ok === true));
|
|
351
|
+
|
|
352
|
+
const started = await settleWithin(start, START_TIMEOUT_MS, null);
|
|
302
353
|
if (!started) return refuse("unreachable");
|
|
303
354
|
if (!started.ok) return refuse(`api_${started.status}`);
|
|
304
355
|
|
|
@@ -29,6 +29,7 @@ jest.mock("@working-theory/license", () => ({
|
|
|
29
29
|
checkLicense: jest.fn(),
|
|
30
30
|
}));
|
|
31
31
|
|
|
32
|
+
import { TOPOLOGY } from "../../../../../lib/topology.generated";
|
|
32
33
|
import { GET } from "../[lookupKey]/route";
|
|
33
34
|
|
|
34
35
|
const mockCheckLicense = checkLicense as jest.MockedFunction<
|
|
@@ -209,6 +210,7 @@ const ENV_NAMES = [
|
|
|
209
210
|
"NODE_ENV",
|
|
210
211
|
"OSS_API_INTERNAL_SECRET",
|
|
211
212
|
"AILK_SPOKE_KEY",
|
|
213
|
+
"VERCEL",
|
|
212
214
|
] as const;
|
|
213
215
|
const savedEnv = Object.fromEntries(ENV_NAMES.map((name) => [name, env[name]]));
|
|
214
216
|
|
|
@@ -248,6 +250,7 @@ beforeEach(() => {
|
|
|
248
250
|
env.AILK_API_URL = API;
|
|
249
251
|
env.NEXT_PUBLIC_BASE_URL = "https://shop.example.com";
|
|
250
252
|
env.NODE_ENV = "test";
|
|
253
|
+
env.VERCEL = "1";
|
|
251
254
|
shownAnswer = SHOWN;
|
|
252
255
|
postAnswer = STARTED;
|
|
253
256
|
fetchMock.mockClear();
|
|
@@ -267,8 +270,9 @@ afterEach(() => {
|
|
|
267
270
|
|
|
268
271
|
/**
|
|
269
272
|
* A buyer's card click on `key`: a top-level same-origin navigation to the
|
|
270
|
-
* wrong host, with a session cookie and
|
|
271
|
-
* set to `null` is
|
|
273
|
+
* wrong host, with a session cookie and fresh forwarded addresses, Vercel's
|
|
274
|
+
* included, so no two cases share a visitor budget. A header set to `null` is
|
|
275
|
+
* left out.
|
|
272
276
|
*/
|
|
273
277
|
function buyRequest(
|
|
274
278
|
key: string,
|
|
@@ -281,6 +285,7 @@ function buyRequest(
|
|
|
281
285
|
"sec-fetch-dest": "document",
|
|
282
286
|
"sec-fetch-site": "same-origin",
|
|
283
287
|
"x-forwarded-for": freshAddress(),
|
|
288
|
+
"x-vercel-forwarded-for": freshAddress(),
|
|
284
289
|
...options.headers,
|
|
285
290
|
};
|
|
286
291
|
const sent = new Headers();
|
|
@@ -411,11 +416,18 @@ describe("AC-1 — a shown key on a connected site starts its Checkout", () => {
|
|
|
411
416
|
);
|
|
412
417
|
});
|
|
413
418
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
+
// #9601 — hub-only: a spoke render of the seam withholds the secret even
|
|
420
|
+
// with no spoke key, so a spoke scaffold skips this case. The sibling
|
|
421
|
+
// suite's "a spoke with no spoke key never sends the internal secret"
|
|
422
|
+
// pins the spoke side.
|
|
423
|
+
(TOPOLOGY === "hub" ? it : it.skip)(
|
|
424
|
+
"sends x-internal-auth from OSS_API_INTERNAL_SECRET to an https API",
|
|
425
|
+
async () => {
|
|
426
|
+
env.OSS_API_INTERNAL_SECRET = "s3cret";
|
|
427
|
+
await expectStarted(await buy("growth_monthly"));
|
|
428
|
+
expect(postHeaders(postCalls()[0]!)["x-internal-auth"]).toBe("s3cret");
|
|
429
|
+
},
|
|
430
|
+
);
|
|
419
431
|
|
|
420
432
|
it("sends no x-internal-auth when OSS_API_INTERNAL_SECRET is unset", async () => {
|
|
421
433
|
await expectStarted(await buy("growth_monthly"));
|
|
@@ -649,6 +661,46 @@ describe("AC-4 — every other outcome returns to /pricing, without throwing and
|
|
|
649
661
|
|
|
650
662
|
// ─── AC-5 ───────────────────────────────────────────────────────────────────
|
|
651
663
|
|
|
664
|
+
// ── #10028 AC-10 — the bundle link's `?with=` ───────────────────────────────
|
|
665
|
+
|
|
666
|
+
describe("#10028 AC-10 — a shown ?with= key is forwarded as requires; an ungrammatical or unshown one is refused", () => {
|
|
667
|
+
/** A buyer's click on a bundle link: the plan key with `?with=<required>` in the URL. */
|
|
668
|
+
function bundleRequest(key: string, withKey: string): Request {
|
|
669
|
+
const base = buyRequest(key);
|
|
670
|
+
return new Request(`${base.url}?with=${withKey}`, { method: "GET", headers: base.headers });
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
it("sends the shown ?with= key as the body's requires, beside the plan key", async () => {
|
|
674
|
+
const res = await buy("growth_monthly", bundleRequest("growth_monthly", "growth_yearly"));
|
|
675
|
+
await expectStarted(res);
|
|
676
|
+
const [call] = postCalls();
|
|
677
|
+
expect(JSON.parse(String(call!.init.body))).toEqual({
|
|
678
|
+
productCode: "growth_monthly",
|
|
679
|
+
requires: "growth_yearly",
|
|
680
|
+
successUrl: "https://shop.example.com/purchase/success?session_id={CHECKOUT_SESSION_ID}",
|
|
681
|
+
cancelUrl: "https://shop.example.com/pricing",
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("builds exactly today's body with no ?with=", async () => {
|
|
686
|
+
await expectStarted(await buy("growth_monthly"));
|
|
687
|
+
const [call] = postCalls();
|
|
688
|
+
expect(JSON.parse(String(call!.init.body))).not.toHaveProperty("requires");
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it("refuses an ungrammatical ?with= before any call", async () => {
|
|
692
|
+
const res = await buy("growth_monthly", bundleRequest("growth_monthly", "Growth%20Yearly"));
|
|
693
|
+
await expectRefused(res, "Growth Yearly", "ungrammatical");
|
|
694
|
+
expect(postCalls()).toHaveLength(0);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it("refuses a grammatical ?with= the shown set does not hold, with no POST", async () => {
|
|
698
|
+
const res = await buy("growth_monthly", bundleRequest("growth_monthly", "managed_monthly"));
|
|
699
|
+
await expectRefused(res, "managed_monthly", "not_shown");
|
|
700
|
+
expect(postCalls()).toHaveLength(0);
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
652
704
|
describe("AC-5 — only a buyer's page load starts anything", () => {
|
|
653
705
|
it.each<
|
|
654
706
|
[string, { method?: string; headers?: Record<string, string | null> }]
|
|
@@ -735,7 +787,9 @@ describe("AC-6 — one shared read, a switched-off product stops within it, a pe
|
|
|
735
787
|
|
|
736
788
|
it("starts at most 5 Checkouts per visitor per 10 minutes", async () => {
|
|
737
789
|
const from = (address: string) =>
|
|
738
|
-
buyRequest("growth_monthly", {
|
|
790
|
+
buyRequest("growth_monthly", {
|
|
791
|
+
headers: { "x-vercel-forwarded-for": address },
|
|
792
|
+
});
|
|
739
793
|
|
|
740
794
|
for (let i = 0; i < 5; i += 1) {
|
|
741
795
|
await expectStarted(await buy("growth_monthly", from("192.0.2.50")));
|
|
@@ -757,43 +811,27 @@ describe("AC-6 — one shared read, a switched-off product stops within it, a pe
|
|
|
757
811
|
expect(postCalls()).toHaveLength(7);
|
|
758
812
|
});
|
|
759
813
|
|
|
760
|
-
it("
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
),
|
|
770
|
-
);
|
|
814
|
+
it("charges a request with no visitor address to one shared budget, ten times as wide", async () => {
|
|
815
|
+
jest.setSystemTime(Date.now() + 10 * 60_000);
|
|
816
|
+
const addressless = () =>
|
|
817
|
+
buyRequest("growth_monthly", {
|
|
818
|
+
headers: { "x-forwarded-for": null, "x-vercel-forwarded-for": null },
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
for (let i = 0; i < 50; i += 1) {
|
|
822
|
+
await expectStarted(await buy("growth_monthly", addressless()));
|
|
771
823
|
}
|
|
824
|
+
expect(postCalls()).toHaveLength(50);
|
|
825
|
+
|
|
772
826
|
await expectRefused(
|
|
773
|
-
await buy(
|
|
774
|
-
"growth_monthly",
|
|
775
|
-
buyRequest("growth_monthly", {
|
|
776
|
-
headers: { "x-forwarded-for": `${prefix}x` },
|
|
777
|
-
}),
|
|
778
|
-
),
|
|
827
|
+
await buy("growth_monthly", addressless()),
|
|
779
828
|
"growth_monthly",
|
|
780
829
|
"rate_limited",
|
|
781
830
|
);
|
|
782
|
-
expect(postCalls()).toHaveLength(
|
|
783
|
-
});
|
|
831
|
+
expect(postCalls()).toHaveLength(50);
|
|
784
832
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
await expectStarted(
|
|
788
|
-
await buy(
|
|
789
|
-
"growth_monthly",
|
|
790
|
-
buyRequest("growth_monthly", {
|
|
791
|
-
headers: { "x-forwarded-for": null },
|
|
792
|
-
}),
|
|
793
|
-
),
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
expect(postCalls()).toHaveLength(7);
|
|
833
|
+
await expectStarted(await buy("growth_monthly"));
|
|
834
|
+
expect(postCalls()).toHaveLength(51);
|
|
797
835
|
});
|
|
798
836
|
|
|
799
837
|
it("reaches neither Stripe nor the session cookie itself", () => {
|
|
@@ -19,14 +19,30 @@
|
|
|
19
19
|
* `getBlogPages` deps — tests inject stubs, and the module's variant of this
|
|
20
20
|
* route supplies the real `blogSource` reader. The exported `GET` is
|
|
21
21
|
* unchanged either way.
|
|
22
|
+
*
|
|
23
|
+
* Docs (#10041 D8): listed after the blog at the site's docs path. Under the
|
|
24
|
+
* database adapter the adapter's `DocsList` / `DocsDetail` rows are listed at
|
|
25
|
+
* the docs path; otherwise those rows are skipped (the MDX docs route never
|
|
26
|
+
* serves them) and the docs directory is listed (`getDocsPages`, default
|
|
27
|
+
* `listDocsPages`).
|
|
22
28
|
*/
|
|
23
29
|
|
|
24
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
SCHEMA_ORG_TYPES,
|
|
32
|
+
resolvedAdapterKind,
|
|
33
|
+
} from "@working-theory/content-adapters";
|
|
25
34
|
import type { ContentAdapter, SchemaOrgType } from "@working-theory/content-adapters";
|
|
26
35
|
import { getContentAdapter as defaultGetContentAdapter } from "@working-theory/page-renderer";
|
|
27
36
|
|
|
37
|
+
import {
|
|
38
|
+
adapterDocsUrl,
|
|
39
|
+
isDocsType,
|
|
40
|
+
listDocsPages,
|
|
41
|
+
type DocsIndexPage,
|
|
42
|
+
} from "../../lib/docs-index";
|
|
43
|
+
import { PUBLIC_PAGES } from "../../lib/modules.generated";
|
|
28
44
|
import { SITE_NAME } from "../../lib/site-brand";
|
|
29
|
-
import {
|
|
45
|
+
import { pagePathFor, resolveBaseUrl } from "../../src/lib/routes";
|
|
30
46
|
|
|
31
47
|
export interface LlmsHandlerDeps {
|
|
32
48
|
/**
|
|
@@ -40,6 +56,8 @@ export interface LlmsHandlerDeps {
|
|
|
40
56
|
* Override in tests to inject stub pages.
|
|
41
57
|
*/
|
|
42
58
|
getBlogPages?: () => Array<{ url: string; data: { title: string; description?: string } }>;
|
|
59
|
+
/** The docs pages (#10041). Defaults to `listDocsPages` over this app's docs directory. */
|
|
60
|
+
getDocsPages?: () => DocsIndexPage[];
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
/**
|
|
@@ -49,8 +67,11 @@ export interface LlmsHandlerDeps {
|
|
|
49
67
|
export function createLlmsHandler(deps: LlmsHandlerDeps = {}) {
|
|
50
68
|
const resolveAdapter = deps.getContentAdapter ?? defaultGetContentAdapter;
|
|
51
69
|
const resolveBlogPages = deps.getBlogPages ?? (() => []);
|
|
70
|
+
const resolveDocsPages = deps.getDocsPages ?? listDocsPages;
|
|
52
71
|
|
|
53
72
|
return async function llmsGET(): Promise<Response> {
|
|
73
|
+
// #10040 decision 7 — no public page, no content index: 404 before the adapter.
|
|
74
|
+
if (!PUBLIC_PAGES) return new Response(null, { status: 404 });
|
|
54
75
|
const adapter = resolveAdapter();
|
|
55
76
|
const baseUrl = resolveBaseUrl();
|
|
56
77
|
const lines: string[] = [`# ${SITE_NAME} — content index`, ""];
|
|
@@ -66,9 +87,13 @@ export function createLlmsHandler(deps: LlmsHandlerDeps = {}) {
|
|
|
66
87
|
types.map((type) => adapter.listPages(type, defaultLocale)),
|
|
67
88
|
);
|
|
68
89
|
|
|
90
|
+
const databaseDocs = resolvedAdapterKind() === "database";
|
|
69
91
|
for (const pages of pageLists) {
|
|
70
92
|
for (const page of pages) {
|
|
71
|
-
|
|
93
|
+
if (isDocsType(page.type) && !databaseDocs) continue;
|
|
94
|
+
const path = isDocsType(page.type)
|
|
95
|
+
? adapterDocsUrl(page.type, page.slug)
|
|
96
|
+
: pagePathFor(page);
|
|
72
97
|
const url = `${baseUrl}${path}`;
|
|
73
98
|
lines.push(`- Title: ${page.title}`);
|
|
74
99
|
lines.push(` URL: ${url}`);
|
|
@@ -92,6 +117,19 @@ export function createLlmsHandler(deps: LlmsHandlerDeps = {}) {
|
|
|
92
117
|
lines.push("");
|
|
93
118
|
}
|
|
94
119
|
|
|
120
|
+
// The docs directory (#10041 D8) — not under the database adapter, whose
|
|
121
|
+
// docs rows are listed above.
|
|
122
|
+
if (!databaseDocs) {
|
|
123
|
+
for (const doc of resolveDocsPages()) {
|
|
124
|
+
lines.push(`- Title: ${doc.title}`);
|
|
125
|
+
lines.push(` URL: ${baseUrl}${doc.url}`);
|
|
126
|
+
if (doc.description) {
|
|
127
|
+
lines.push(` Description: ${doc.description}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push("");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
95
133
|
return new Response(lines.join("\n"), {
|
|
96
134
|
status: 200,
|
|
97
135
|
headers: {
|
|
@@ -15,12 +15,16 @@
|
|
|
15
15
|
|
|
16
16
|
import { parseStealth } from "@working-theory/web-i18n/stealth";
|
|
17
17
|
|
|
18
|
+
import { PUBLIC_PAGES } from "../../lib/modules.generated";
|
|
18
19
|
import { resolveBaseUrl } from "../../src/lib/routes";
|
|
19
20
|
|
|
20
21
|
export function GET(): Response {
|
|
21
22
|
const baseUrl = resolveBaseUrl();
|
|
22
23
|
const disallowEnv = process.env["AILK_ROBOTS_DISALLOW"] ?? "";
|
|
23
|
-
|
|
24
|
+
// #10040 decision 7 — a site with neither marketing nor docs-blog (a hub)
|
|
25
|
+
// has no public page to crawl: the same disallow-all, no-Sitemap shape as
|
|
26
|
+
// STEALTH, read from the module list, never from a setting.
|
|
27
|
+
const stealth = parseStealth(process.env["STEALTH"]) || !PUBLIC_PAGES;
|
|
24
28
|
|
|
25
29
|
const disallowPaths = stealth
|
|
26
30
|
? ["/"]
|
|
@@ -28,13 +28,30 @@
|
|
|
28
28
|
* DI note: `createSitemapHandler` accepts an optional `getContentAdapter` dep
|
|
29
29
|
* for testing — tests inject a stub adapter to avoid mocking the internal module.
|
|
30
30
|
* The exported `GET` uses the real implementation.
|
|
31
|
+
*
|
|
32
|
+
* Docs (#10041 D8): the docs are listed at the site's docs path, default
|
|
33
|
+
* locale only, with no alternates (they exist in one locale). Under the
|
|
34
|
+
* database adapter the docs route reads the adapter's `DocsList` /
|
|
35
|
+
* `DocsDetail` rows, so those rows are listed at the docs path; otherwise
|
|
36
|
+
* those rows are skipped (the MDX docs route never serves them) and the docs
|
|
37
|
+
* directory is listed instead (`getDocsPages`, default `listDocsPages`).
|
|
31
38
|
*/
|
|
32
39
|
|
|
33
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
SCHEMA_ORG_TYPES,
|
|
42
|
+
resolvedAdapterKind,
|
|
43
|
+
} from "@working-theory/content-adapters";
|
|
34
44
|
import type { ContentAdapter, SchemaOrgType } from "@working-theory/content-adapters";
|
|
35
45
|
import { getContentAdapter as defaultGetContentAdapter } from "@working-theory/page-renderer";
|
|
36
46
|
|
|
37
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
adapterDocsUrl,
|
|
49
|
+
isDocsType,
|
|
50
|
+
listDocsPages,
|
|
51
|
+
type DocsIndexPage,
|
|
52
|
+
} from "../../lib/docs-index";
|
|
53
|
+
import { PUBLIC_PAGES } from "../../lib/modules.generated";
|
|
54
|
+
import { pagePathFor, resolveBaseUrl } from "../../src/lib/routes";
|
|
38
55
|
|
|
39
56
|
export interface SitemapHandlerDeps {
|
|
40
57
|
/**
|
|
@@ -42,6 +59,8 @@ export interface SitemapHandlerDeps {
|
|
|
42
59
|
* Override in tests to inject a stub adapter without mocking the internal module.
|
|
43
60
|
*/
|
|
44
61
|
getContentAdapter?: () => ContentAdapter;
|
|
62
|
+
/** The docs pages (#10041). Defaults to `listDocsPages` over this app's docs directory. */
|
|
63
|
+
getDocsPages?: () => DocsIndexPage[];
|
|
45
64
|
}
|
|
46
65
|
|
|
47
66
|
function escapeXml(value: string): string {
|
|
@@ -76,7 +95,10 @@ function buildLocaleUrl(
|
|
|
76
95
|
*/
|
|
77
96
|
export function createSitemapHandler(deps: SitemapHandlerDeps = {}) {
|
|
78
97
|
const resolveAdapter = deps.getContentAdapter ?? defaultGetContentAdapter;
|
|
98
|
+
const resolveDocsPages = deps.getDocsPages ?? listDocsPages;
|
|
79
99
|
return async function sitemapGET(): Promise<Response> {
|
|
100
|
+
// #10040 decision 7 — no public page, no sitemap: 404 before the adapter.
|
|
101
|
+
if (!PUBLIC_PAGES) return new Response(null, { status: 404 });
|
|
80
102
|
const adapter = resolveAdapter();
|
|
81
103
|
const baseUrl = resolveBaseUrl();
|
|
82
104
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -87,6 +109,13 @@ export function createSitemapHandler(deps: SitemapHandlerDeps = {}) {
|
|
|
87
109
|
const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? "en";
|
|
88
110
|
|
|
89
111
|
const urlEntries: string[] = [];
|
|
112
|
+
// A docs entry: one locale, so no alternates (#10041 D8).
|
|
113
|
+
const pushDocsEntry = (url: string, lastmod: string): void => {
|
|
114
|
+
urlEntries.push(
|
|
115
|
+
` <url>\n <loc>${escapeXml(`${baseUrl}${url}`)}</loc>\n <lastmod>${lastmod}</lastmod>\n </url>`,
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
const databaseDocs = resolvedAdapterKind() === "database";
|
|
90
119
|
|
|
91
120
|
const types = SCHEMA_ORG_TYPES as readonly SchemaOrgType[];
|
|
92
121
|
|
|
@@ -104,7 +133,16 @@ export function createSitemapHandler(deps: SitemapHandlerDeps = {}) {
|
|
|
104
133
|
for (let i = 0; i < tuples.length; i++) {
|
|
105
134
|
const { locale } = tuples[i]!;
|
|
106
135
|
for (const page of lists[i]!) {
|
|
107
|
-
|
|
136
|
+
if (isDocsType(page.type)) {
|
|
137
|
+
if (databaseDocs && locale === defaultLocale) {
|
|
138
|
+
pushDocsEntry(
|
|
139
|
+
adapterDocsUrl(page.type, page.slug),
|
|
140
|
+
page.publishedAt ?? today,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const path = pagePathFor(page);
|
|
108
146
|
const lastmod = page.publishedAt ?? today;
|
|
109
147
|
|
|
110
148
|
const defaultUrl = `${baseUrl}${path}`;
|
|
@@ -126,6 +164,10 @@ export function createSitemapHandler(deps: SitemapHandlerDeps = {}) {
|
|
|
126
164
|
}
|
|
127
165
|
}
|
|
128
166
|
|
|
167
|
+
if (!databaseDocs) {
|
|
168
|
+
for (const doc of resolveDocsPages()) pushDocsEntry(doc.url, today);
|
|
169
|
+
}
|
|
170
|
+
|
|
129
171
|
const xml = [
|
|
130
172
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
131
173
|
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">',
|