create-ailk 0.27.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 +84 -7
- 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 +23 -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 +105 -9
- 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 +38 -4
- 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 +5 -2
- package/templates/apps/api/.env.example +9 -5
- package/templates/apps/api/CLAUDE.md +14 -9
- 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/mcp/CLAUDE.md +18 -10
- package/templates/apps/web/.env.example +25 -11
- package/templates/apps/web/CLAUDE.md +45 -4
- 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]/(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 +157 -139
- 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 +204 -2
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +151 -33
- 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 +202 -10
- 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/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 +31 -106
- 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/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 +86 -30
- package/templates/apps/web/middleware.ts +171 -11
- package/templates/apps/web/next.config.mjs +211 -2
- package/templates/apps/web/package.json +1 -1
- 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
|
@@ -295,7 +295,13 @@
|
|
|
295
295
|
"addonsHeadline": "Add-ons",
|
|
296
296
|
"monthly": "{amount}/mo",
|
|
297
297
|
"annual": "{amount}/yr",
|
|
298
|
-
"oneTime": "{amount} one-time"
|
|
298
|
+
"oneTime": "{amount} one-time",
|
|
299
|
+
"withRequired": "{plan} + {required}",
|
|
300
|
+
"depositNote": "{deposit} deposit due today; the balance is invoiced."
|
|
301
|
+
},
|
|
302
|
+
"pricingPages": {
|
|
303
|
+
"heading": "Pricing by offering",
|
|
304
|
+
"viewPricing": "See pricing"
|
|
299
305
|
},
|
|
300
306
|
"purchasePolicy": {
|
|
301
307
|
"heading": "Purchase Policy",
|
|
@@ -393,7 +399,9 @@
|
|
|
393
399
|
},
|
|
394
400
|
"services": {
|
|
395
401
|
"heading": "Services",
|
|
396
|
-
"emptyState": "Services listing coming soon."
|
|
402
|
+
"emptyState": "Services listing coming soon.",
|
|
403
|
+
"childrenHeading": "Services in this area",
|
|
404
|
+
"viewService": "View service"
|
|
397
405
|
},
|
|
398
406
|
"software": {
|
|
399
407
|
"heading": "Software",
|
|
@@ -425,6 +433,17 @@
|
|
|
425
433
|
"heading": "Alternatives",
|
|
426
434
|
"emptyState": "Comparisons coming soon."
|
|
427
435
|
},
|
|
436
|
+
"templates": {
|
|
437
|
+
"heading": "Templates",
|
|
438
|
+
"emptyState": "Templates coming soon.",
|
|
439
|
+
"getIt": "Get it",
|
|
440
|
+
"startFree": "Start free",
|
|
441
|
+
"viewDemo": "View demo",
|
|
442
|
+
"viewTemplate": "View template",
|
|
443
|
+
"included": "What's included",
|
|
444
|
+
"demoBadge": "Live demo",
|
|
445
|
+
"moreCategories": "+{count} more"
|
|
446
|
+
},
|
|
428
447
|
"reports": {
|
|
429
448
|
"heading": "Reports",
|
|
430
449
|
"emptyState": "Reports coming soon.",
|
|
@@ -1256,26 +1275,46 @@
|
|
|
1256
1275
|
"heading": "Integrations",
|
|
1257
1276
|
"lead": "Connect the services your site works with.",
|
|
1258
1277
|
"slack": {
|
|
1259
|
-
"title": "
|
|
1260
|
-
"credentialsHeading": "App credentials",
|
|
1278
|
+
"title": "Connection",
|
|
1261
1279
|
"connectedBadge": "Connected",
|
|
1262
|
-
"
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1280
|
+
"row": {
|
|
1281
|
+
"notConnectedTitle": "Enable team collaboration in Slack",
|
|
1282
|
+
"notConnectedLine": "Connect your Slack workspace so your team can work with this site in Slack.",
|
|
1283
|
+
"connectedTitle": "Site workspace",
|
|
1284
|
+
"connectedLine": "Enable team collaboration in Slack.",
|
|
1285
|
+
"unavailableTitle": "We couldn't check Slack.",
|
|
1286
|
+
"unavailableLine": "The Slack connection didn't load. Try again in a moment."
|
|
1287
|
+
},
|
|
1288
|
+
"connect": "Connect",
|
|
1289
|
+
"tryAgain": "Try again",
|
|
1290
|
+
"moreActions": "More Slack actions",
|
|
1291
|
+
"replace": "Replace…",
|
|
1292
|
+
"disconnect": "Disconnect…",
|
|
1293
|
+
"setup": {
|
|
1294
|
+
"region": "Set up Slack",
|
|
1295
|
+
"tokenStep": "Create a Slack app and paste its bot token",
|
|
1296
|
+
"tokenTip": "The app needs a paid Slack plan and these bot scopes:",
|
|
1297
|
+
"tipLabel": "More about this step",
|
|
1298
|
+
"openSlackApps": "Open Slack apps",
|
|
1299
|
+
"slackAppsHref": "https://api.slack.com/apps",
|
|
1300
|
+
"tokenPlaceholder": "xoxb-…",
|
|
1301
|
+
"tokenSaved": "Bot token saved",
|
|
1302
|
+
"urlStep": "Copy the events URL into Slack",
|
|
1303
|
+
"urlTip": "In your Slack app, open Event Subscriptions, turn on events, paste this as the Request URL, and subscribe to the bot events {events}.",
|
|
1304
|
+
"urlUnavailable": "The API's address isn't configured, so the events URL can't be shown.",
|
|
1305
|
+
"copy": "Copy",
|
|
1306
|
+
"copied": "Copied",
|
|
1307
|
+
"urlCopied": "Events URL copied",
|
|
1308
|
+
"secretStep": "Paste the app’s signing secret",
|
|
1309
|
+
"secretTip": "In your Slack app, under Basic Information → App Credentials.",
|
|
1310
|
+
"secretPlaceholder": "Signing secret",
|
|
1311
|
+
"save": "Save",
|
|
1312
|
+
"saving": "Saving…",
|
|
1313
|
+
"cancel": "Cancel",
|
|
1314
|
+
"done": "Done"
|
|
1315
|
+
},
|
|
1266
1316
|
"botTokenInvalid": "A bot token starts with xoxb- and holds only letters, digits and hyphens.",
|
|
1267
1317
|
"signingSecretInvalid": "A signing secret is 32 to 128 letters and digits.",
|
|
1268
|
-
"scopesLead": "The Slack app needs these bot scopes:",
|
|
1269
|
-
"connect": "Connect Slack",
|
|
1270
|
-
"connecting": "Connecting…",
|
|
1271
|
-
"cancel": "Cancel",
|
|
1272
|
-
"replace": "Replace",
|
|
1273
|
-
"disconnect": "Disconnect",
|
|
1274
|
-
"workspaceName": "Slack workspace",
|
|
1275
|
-
"connectedOn": "Connected",
|
|
1276
|
-
"ownerOnly": "Only the site's owner can change the Slack connection.",
|
|
1277
|
-
"askOwner": "Slack isn't connected. Ask the site's owner to connect it.",
|
|
1278
|
-
"unavailable": "The Slack connection couldn't be read. Try again later.",
|
|
1279
1318
|
"tokenRefused": {
|
|
1280
1319
|
"tokenRefused": "Slack refused this token. Check it and try again.",
|
|
1281
1320
|
"notBotToken": "This isn't a bot token. Copy the Bot User OAuth Token (xoxb-).",
|
|
@@ -1301,15 +1340,16 @@
|
|
|
1301
1340
|
"prospectChannel": {
|
|
1302
1341
|
"heading": "Prospect channels",
|
|
1303
1342
|
"lead": "Open a shared Slack channel with each prospect who books a meeting.",
|
|
1304
|
-
"
|
|
1305
|
-
"
|
|
1306
|
-
"
|
|
1307
|
-
"
|
|
1308
|
-
"
|
|
1309
|
-
"
|
|
1310
|
-
"
|
|
1311
|
-
"
|
|
1312
|
-
"
|
|
1343
|
+
"switchLabel": "Prospect channels",
|
|
1344
|
+
"showSettings": "Show prospect channel settings",
|
|
1345
|
+
"hideSettings": "Hide prospect channel settings",
|
|
1346
|
+
"members": "Your channel team members",
|
|
1347
|
+
"membersTipLabel": "About members",
|
|
1348
|
+
"membersTip": "Slack member ids of teammates to add to each new channel, separated by commas. The site's owner is always added.",
|
|
1349
|
+
"membersPlaceholder": "U012AB3CD, U045EF6GH",
|
|
1350
|
+
"welcomeTemplate": "Welcome message for prospects",
|
|
1351
|
+
"mergeFieldsLabel": "Merge fields",
|
|
1352
|
+
"mergeFieldsLead": "You can use",
|
|
1313
1353
|
"mergeFields": {
|
|
1314
1354
|
"firstName": "The prospect's first name",
|
|
1315
1355
|
"hostName": "The host's name",
|
|
@@ -1318,7 +1358,11 @@
|
|
|
1318
1358
|
"timeline": "The prospect's timeline",
|
|
1319
1359
|
"meetingLink": "The meeting link"
|
|
1320
1360
|
},
|
|
1321
|
-
"
|
|
1361
|
+
"welcomePlaceholder": "Leave blank to send the default welcome.",
|
|
1362
|
+
"channelPrefix": "Channel prefix",
|
|
1363
|
+
"prefixTipLabel": "About the channel prefix",
|
|
1364
|
+
"prefixTip": "Each booking gets its own new Slack channel. Its name starts with this prefix, then a short random code, like {example}, so all prospect channels sort together in Slack.",
|
|
1365
|
+
"prefixExample": "prospect-k3x9m2qa",
|
|
1322
1366
|
"archiveDays": "Archive after (days)",
|
|
1323
1367
|
"save": "Save",
|
|
1324
1368
|
"saving": "Saving…",
|
|
@@ -1327,7 +1371,7 @@
|
|
|
1327
1371
|
"slackNotConnected": "Slack isn't connected, so these settings can't be changed.",
|
|
1328
1372
|
"connectSlackLink": "Connect Slack",
|
|
1329
1373
|
"errors": {
|
|
1330
|
-
"memberIds": "Enter
|
|
1374
|
+
"memberIds": "Enter Slack member ids like U012AB3CD, separated by commas, with no repeats (up to 50).",
|
|
1331
1375
|
"welcomeTemplate": "Use only the merge fields listed, in up to 4,000 characters.",
|
|
1332
1376
|
"channelPrefix": "Use lowercase letters, digits, - and _, starting with a letter or digit (up to 21).",
|
|
1333
1377
|
"archiveDays": "Enter a whole number of days from 1 to 365."
|
|
@@ -1455,5 +1499,17 @@
|
|
|
1455
1499
|
"already": "You were already unsubscribed.",
|
|
1456
1500
|
"invalid": "This link isn't valid.",
|
|
1457
1501
|
"unavailable": "Unsubscribing isn't available right now."
|
|
1502
|
+
},
|
|
1503
|
+
"hubConnect": {
|
|
1504
|
+
"metaTitle": "Sign in to a site",
|
|
1505
|
+
"title": "Sign in to {name}?",
|
|
1506
|
+
"lead": "{origin} will see your name and email address, {email}. We ask once for each site.",
|
|
1507
|
+
"continue": "Continue",
|
|
1508
|
+
"cancel": "Cancel",
|
|
1509
|
+
"unavailable": {
|
|
1510
|
+
"title": "This sign-in can't continue",
|
|
1511
|
+
"body": "The link may have expired. Go back to the site you came from and sign in again.",
|
|
1512
|
+
"back": "Back to your account"
|
|
1513
|
+
}
|
|
1458
1514
|
}
|
|
1459
1515
|
}
|
|
@@ -19,6 +19,29 @@
|
|
|
19
19
|
// here rather than in the matcher, because this middleware is the only place
|
|
20
20
|
// the security headers are stamped.
|
|
21
21
|
//
|
|
22
|
+
// The spoke file routes (#9162) — `/reports/<slug>/file`,
|
|
23
|
+
// `/account/inbox/message/<id>/file` and
|
|
24
|
+
// `/account/artifacts/<pieceId>/versions/<version>/file` — get every baseline
|
|
25
|
+
// header but the CSP. Their handlers set the hub's sandbox policy, and this
|
|
26
|
+
// middleware runs before them: Next puts its headers on the response first and
|
|
27
|
+
// adds a handler header only where none is set yet, so a site CSP stamped here
|
|
28
|
+
// would replace the sandbox one on the wire.
|
|
29
|
+
//
|
|
30
|
+
// Docs path (#10041 D3). A site that sets `docsPath` in `_site.mdx` serves
|
|
31
|
+
// its docs there: a request at or under the public path (bare or after an
|
|
32
|
+
// enabled-locale prefix) is handed to next-intl as the internal `/docs` route,
|
|
33
|
+
// so its rewrite lands on `app/[locale]/docs/` while the browser keeps the
|
|
34
|
+
// public URL; a redirect's `Location` and every `Link` hreflang entry are
|
|
35
|
+
// mapped back onto the public path. The internal `/docs` itself answers with
|
|
36
|
+
// the site's 404, before next-intl, so no locale redirect intervenes. A site
|
|
37
|
+
// with no `docsPath` takes none of these branches.
|
|
38
|
+
//
|
|
39
|
+
// Services path (#10043 D5). The same mapping for a site that sets
|
|
40
|
+
// `services.path` in `_site.mdx`, except that the internal `/services` (and
|
|
41
|
+
// everything under it) answers a 308 onto the same rest under the public
|
|
42
|
+
// base, so old links keep working. A site with no `services.path` takes none
|
|
43
|
+
// of these branches.
|
|
44
|
+
//
|
|
22
45
|
// S16 spec: docs/internal/specs/ailk-pre-oss-security-phase-3-s16-security-headers-spec.md
|
|
23
46
|
// S2 spec: docs/internal/specs/i18n-phase1-s2-next-intl-foundation-spec.md
|
|
24
47
|
// P2 S1 spec: docs/internal/specs/i18n-phase2-s1-hreflang-metadata-spec.md §3
|
|
@@ -27,7 +50,19 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
27
50
|
import createMiddleware from "next-intl/middleware";
|
|
28
51
|
|
|
29
52
|
import { isBookingPath } from "./lib/auth-routes";
|
|
30
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
DOCS_PATH,
|
|
55
|
+
DOCS_ROUTE,
|
|
56
|
+
toInternalDocsPath,
|
|
57
|
+
toPublicDocsHref,
|
|
58
|
+
} from "./lib/docs-path";
|
|
59
|
+
import { PUBLIC_PAGES, SCHEDULING_PAGES } from "./lib/modules.generated";
|
|
60
|
+
import {
|
|
61
|
+
SERVICES_PATH,
|
|
62
|
+
SERVICES_ROUTE,
|
|
63
|
+
toInternalServicesPath,
|
|
64
|
+
toPublicServicesHref,
|
|
65
|
+
} from "./lib/services-path";
|
|
31
66
|
import {
|
|
32
67
|
DEFAULT_SECURITY_HEADERS,
|
|
33
68
|
SCHEDULING_SECURITY_HEADERS,
|
|
@@ -70,6 +105,76 @@ export function isNonLocaleRoute(pathname: string): boolean {
|
|
|
70
105
|
);
|
|
71
106
|
}
|
|
72
107
|
|
|
108
|
+
// The file routes' paths after an enabled-locale prefix; a route group adds no
|
|
109
|
+
// segment, so `(authed)` is not in them. Kept in step with every route handler
|
|
110
|
+
// that sets its own CSP by the drift test in
|
|
111
|
+
// __tests__/middleware-file-routes.test.ts.
|
|
112
|
+
const FILE_ROUTE_PATHS = [
|
|
113
|
+
/^\/reports\/[^/]+\/file$/,
|
|
114
|
+
/^\/account\/inbox\/message\/[^/]+\/file$/,
|
|
115
|
+
/^\/account\/artifacts\/[^/]+\/versions\/[^/]+\/file$/,
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const LOCALE_PREFIX = new RegExp(`^/(${enabledLocales.join("|")})(?=/)`);
|
|
119
|
+
|
|
120
|
+
export function isFileRoutePath(pathname: string): boolean {
|
|
121
|
+
const bare = pathname.replace(LOCALE_PREFIX, "");
|
|
122
|
+
return FILE_ROUTE_PATHS.some((path) => path.test(bare));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The path the hidden docs route is rewritten to. Its first segment is no
|
|
126
|
+
// enabled locale, so the locale layout's `assertEnabledLocale` answers it with
|
|
127
|
+
// the site's not-found page (#10041 D3).
|
|
128
|
+
const DOCS_UNMOUNTED_PATH = "/_ailk-docs-unmounted";
|
|
129
|
+
|
|
130
|
+
function splitLocalePrefix(pathname: string): { prefix: string; bare: string } {
|
|
131
|
+
const prefix = LOCALE_PREFIX.exec(pathname)?.[0] ?? "";
|
|
132
|
+
return { prefix, bare: pathname.slice(prefix.length) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* `url` (absolute) with its locale-less pathname mapped back onto a public
|
|
137
|
+
* path by `toPublicHref` — `toPublicDocsHref` or `toPublicServicesHref`.
|
|
138
|
+
*/
|
|
139
|
+
function toPublicUrl(url: string, toPublicHref: (bare: string) => string): string {
|
|
140
|
+
const parsed = new URL(url);
|
|
141
|
+
const { prefix, bare } = splitLocalePrefix(parsed.pathname);
|
|
142
|
+
parsed.pathname = prefix + toPublicHref(bare);
|
|
143
|
+
return parsed.toString();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* next-intl's response for a mapped docs or services request, with the
|
|
148
|
+
* internal path it was handed mapped back onto the public one: a redirect's
|
|
149
|
+
* `Location` and each `<url>` of the hreflang `Link` header. A rewrite is
|
|
150
|
+
* left as is.
|
|
151
|
+
*/
|
|
152
|
+
function withPublicUrls(
|
|
153
|
+
response: NextResponse,
|
|
154
|
+
toPublicHref: (bare: string) => string,
|
|
155
|
+
): NextResponse {
|
|
156
|
+
const location = response.headers.get("location");
|
|
157
|
+
if (location) response.headers.set("location", toPublicUrl(location, toPublicHref));
|
|
158
|
+
const link = response.headers.get("Link");
|
|
159
|
+
if (link) {
|
|
160
|
+
response.headers.set(
|
|
161
|
+
"Link",
|
|
162
|
+
link.replace(/<([^>]+)>/g, (_entry, url: string) => `<${toPublicUrl(url, toPublicHref)}>`),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return response;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The baseline map without its CSP, for the file routes. */
|
|
169
|
+
const FILE_ROUTE_SECURITY_HEADERS: Readonly<Record<string, string>> =
|
|
170
|
+
Object.freeze(
|
|
171
|
+
Object.fromEntries(
|
|
172
|
+
Object.entries(DEFAULT_SECURITY_HEADERS).filter(
|
|
173
|
+
([name]) => name !== "Content-Security-Policy",
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
|
|
73
178
|
export function middleware(request: NextRequest): NextResponse {
|
|
74
179
|
// Step 1: Forward x-pathname to server components so generateMetadata can
|
|
75
180
|
// build path-aware hreflang alternates (P2 S1). Clone the request headers
|
|
@@ -82,27 +187,82 @@ export function middleware(request: NextRequest): NextResponse {
|
|
|
82
187
|
if (isNonLocaleRoute(request.nextUrl.pathname)) {
|
|
83
188
|
const res = NextResponse.next({ request: { headers: requestHeaders } });
|
|
84
189
|
applyHeaders(res.headers, DEFAULT_SECURITY_HEADERS);
|
|
190
|
+
if (!PUBLIC_PAGES) res.headers.set("X-Robots-Tag", "noindex");
|
|
85
191
|
return res;
|
|
86
192
|
}
|
|
87
193
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
194
|
+
// #10041 D3 — on a site that moved its docs, map the public docs path onto
|
|
195
|
+
// the internal route, and answer the internal route itself with the 404.
|
|
196
|
+
let docsInternalUrl: URL | null = null;
|
|
197
|
+
if (DOCS_PATH !== DOCS_ROUTE) {
|
|
198
|
+
const { prefix, bare } = splitLocalePrefix(request.nextUrl.pathname);
|
|
199
|
+
const internal = toInternalDocsPath(bare);
|
|
200
|
+
if (internal !== null) {
|
|
201
|
+
docsInternalUrl = request.nextUrl.clone();
|
|
202
|
+
docsInternalUrl.pathname = prefix + internal;
|
|
203
|
+
} else if (bare === DOCS_ROUTE || bare.startsWith(`${DOCS_ROUTE}/`)) {
|
|
204
|
+
const res = NextResponse.rewrite(new URL(DOCS_UNMOUNTED_PATH, request.url), {
|
|
205
|
+
request: { headers: requestHeaders },
|
|
206
|
+
});
|
|
207
|
+
applyHeaders(res.headers, DEFAULT_SECURITY_HEADERS);
|
|
208
|
+
if (!PUBLIC_PAGES) res.headers.set("X-Robots-Tag", "noindex");
|
|
209
|
+
return res;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// #10043 D5 — on a site that moved its services, map the public base onto
|
|
214
|
+
// the internal route, and answer the internal route itself with a 308 onto
|
|
215
|
+
// the base (same origin, same locale prefix and query, so it cannot leave
|
|
216
|
+
// the site).
|
|
217
|
+
let servicesInternalUrl: URL | null = null;
|
|
218
|
+
if (docsInternalUrl === null && SERVICES_PATH !== SERVICES_ROUTE) {
|
|
219
|
+
const { prefix, bare } = splitLocalePrefix(request.nextUrl.pathname);
|
|
220
|
+
const internal = toInternalServicesPath(bare);
|
|
221
|
+
if (internal !== null) {
|
|
222
|
+
servicesInternalUrl = request.nextUrl.clone();
|
|
223
|
+
servicesInternalUrl.pathname = prefix + internal;
|
|
224
|
+
} else if (bare === SERVICES_ROUTE || bare.startsWith(`${SERVICES_ROUTE}/`)) {
|
|
225
|
+
const target = request.nextUrl.clone();
|
|
226
|
+
target.pathname = prefix + toPublicServicesHref(bare);
|
|
227
|
+
const res = NextResponse.redirect(target, 308);
|
|
228
|
+
applyHeaders(res.headers, DEFAULT_SECURITY_HEADERS);
|
|
229
|
+
if (!PUBLIC_PAGES) res.headers.set("X-Robots-Tag", "noindex");
|
|
230
|
+
return res;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const requestWithPathname = new NextRequest(
|
|
235
|
+
docsInternalUrl ?? servicesInternalUrl ?? request.url,
|
|
236
|
+
{
|
|
237
|
+
headers: requestHeaders,
|
|
238
|
+
method: request.method,
|
|
239
|
+
body: request.body,
|
|
240
|
+
},
|
|
241
|
+
);
|
|
93
242
|
|
|
94
243
|
// Step 2: run next-intl middleware (locale detection + rewrite/redirect)
|
|
95
|
-
|
|
244
|
+
let intlResponse = intlMiddleware(requestWithPathname) as NextResponse;
|
|
245
|
+
if (docsInternalUrl) intlResponse = withPublicUrls(intlResponse, toPublicDocsHref);
|
|
246
|
+
if (servicesInternalUrl) {
|
|
247
|
+
intlResponse = withPublicUrls(intlResponse, toPublicServicesHref);
|
|
248
|
+
}
|
|
96
249
|
|
|
97
250
|
// Step 3: apply the 7 baseline security headers to the response. The booking
|
|
98
251
|
// pages of a site that carries the scheduling module get the same map with
|
|
99
|
-
// Cal.com's origin added to the CSP (#8843 D12); no other page does.
|
|
252
|
+
// Cal.com's origin added to the CSP (#8843 D12); no other page does. The
|
|
253
|
+
// file routes get the six others and keep their own sandbox CSP (#9162).
|
|
100
254
|
applyHeaders(
|
|
101
255
|
intlResponse.headers,
|
|
102
|
-
|
|
103
|
-
?
|
|
104
|
-
:
|
|
256
|
+
isFileRoutePath(request.nextUrl.pathname)
|
|
257
|
+
? FILE_ROUTE_SECURITY_HEADERS
|
|
258
|
+
: SCHEDULING_PAGES &&
|
|
259
|
+
isBookingPath(request.nextUrl.pathname, enabledLocales)
|
|
260
|
+
? SCHEDULING_SECURITY_HEADERS
|
|
261
|
+
: DEFAULT_SECURITY_HEADERS,
|
|
105
262
|
);
|
|
263
|
+
// #10040 decision 7 — a site with neither marketing nor docs-blog (a hub)
|
|
264
|
+
// asks every crawler to keep out of every page, read from the module list.
|
|
265
|
+
if (!PUBLIC_PAGES) intlResponse.headers.set("X-Robots-Tag", "noindex");
|
|
106
266
|
|
|
107
267
|
return intlResponse;
|
|
108
268
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
4
|
import bundleAnalyzer from "@next/bundle-analyzer";
|
|
5
5
|
import { withSentryConfig } from "@sentry/nextjs";
|
|
6
6
|
import createNextIntlPlugin from "next-intl/plugin";
|
|
7
|
-
import {
|
|
7
|
+
import { DocsPathSchema, ServicesPathSchema } from "@working-theory/site-config/schema";
|
|
8
|
+
import { legacyRedirects, ROUTES, SERVICES_ROUTE } from "@working-theory/templates/routes";
|
|
8
9
|
import { parseStealth } from "@working-theory/web-i18n/stealth";
|
|
9
10
|
import { withBotId } from "botid/next/config";
|
|
10
11
|
|
|
@@ -81,6 +82,203 @@ function readDefaultLocaleFromProjectYaml() {
|
|
|
81
82
|
const enabledLocalesAtBuild = readLocalesFromProjectYaml();
|
|
82
83
|
const defaultLocaleAtBuild = readDefaultLocaleFromProjectYaml();
|
|
83
84
|
|
|
85
|
+
// #10041 D4 — where the docs live. A site sets `docsPath` in
|
|
86
|
+
// content/_site.mdx (its own file, which `create-ailk update` never rewrites);
|
|
87
|
+
// it is read here, refused by name when it cannot be served, and baked into
|
|
88
|
+
// NEXT_PUBLIC_AILK_DOCS_PATH for the middleware and every docs-URL reader
|
|
89
|
+
// (lib/docs-path.ts). Absent — the file, its frontmatter or the key — is
|
|
90
|
+
// `/docs`, today's path. A refusal throws, so `next build` fails before a page
|
|
91
|
+
// renders (the parseStealth posture).
|
|
92
|
+
const DOCS_ROUTE = "/docs";
|
|
93
|
+
|
|
94
|
+
function refuseDocsPath(value, reason) {
|
|
95
|
+
throw new Error(`content/_site.mdx docsPath ${value}: ${reason}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The mount checks a public path takes before it is served (#10041 D4, shared
|
|
100
|
+
* with `services.path` by #10043 D4), each refused through `refuse`:
|
|
101
|
+
*
|
|
102
|
+
* 1. A locale code as the first segment would be read as the locale.
|
|
103
|
+
* 2. A path that shadows another page, or sits under a catch-all. A route is
|
|
104
|
+
* shadowed when the path matches its leading segments, a `:param` segment
|
|
105
|
+
* matching any one segment (`/products/docs` hides the `/products/:slug`
|
|
106
|
+
* page whose slug is `docs`). `ownRoutes` are the setting's own rows.
|
|
107
|
+
*/
|
|
108
|
+
function refuseUnmountablePath(value, ownRoutes, refuse) {
|
|
109
|
+
const firstSegment = value.split("/")[1];
|
|
110
|
+
const locales = enabledLocalesAtBuild.split(",").map((l) => l.toLowerCase());
|
|
111
|
+
if (locales.includes(firstSegment)) {
|
|
112
|
+
refuse(value, `its first segment is the enabled locale ${firstSegment}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const valueSegments = value.split("/").slice(1);
|
|
116
|
+
for (const { route } of ROUTES) {
|
|
117
|
+
if (ownRoutes.includes(route)) continue;
|
|
118
|
+
const catchAll = /^(.*)\/:\.\.\.[^/]+$/.exec(route);
|
|
119
|
+
if (catchAll) {
|
|
120
|
+
if (value.startsWith(`${catchAll[1]}/`)) {
|
|
121
|
+
refuse(value, `it is under the ${catchAll[1]} catch-all route`);
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const routeSegments = route.split("/").slice(1);
|
|
126
|
+
const shadows =
|
|
127
|
+
valueSegments.length <= routeSegments.length &&
|
|
128
|
+
valueSegments.every(
|
|
129
|
+
(segment, i) =>
|
|
130
|
+
routeSegments[i].startsWith(":") || routeSegments[i] === segment,
|
|
131
|
+
);
|
|
132
|
+
if (shadows) refuse(value, `it shadows the ${route} route`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const contentRootAtBuild =
|
|
137
|
+
process.env.AILK_CONTENT_ROOT || join(process.cwd(), "../../content");
|
|
138
|
+
|
|
139
|
+
/** A file's YAML frontmatter, parsed; `undefined` when absent or unreadable. */
|
|
140
|
+
function readFrontmatter(path) {
|
|
141
|
+
let raw;
|
|
142
|
+
try {
|
|
143
|
+
raw = readFileSync(path, "utf8");
|
|
144
|
+
} catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
// The frontmatter is the text between the first two `---` lines.
|
|
148
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw);
|
|
149
|
+
return match ? parseYaml(match[1]) : undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const siteFrontmatterAtBuild = readFrontmatter(join(contentRootAtBuild, "_site.mdx"));
|
|
153
|
+
|
|
154
|
+
function readDocsPathFromSiteMdx() {
|
|
155
|
+
const value = siteFrontmatterAtBuild?.docsPath;
|
|
156
|
+
if (value === undefined || value === null) return DOCS_ROUTE;
|
|
157
|
+
|
|
158
|
+
const parsed = DocsPathSchema.safeParse(value);
|
|
159
|
+
if (!parsed.success) {
|
|
160
|
+
refuseDocsPath(
|
|
161
|
+
value,
|
|
162
|
+
"must be one to four lowercase kebab segments with a leading slash and no trailing slash, not starting with api, og or v1, and not under /docs",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const docsPath = parsed.data;
|
|
166
|
+
refuseUnmountablePath(
|
|
167
|
+
docsPath,
|
|
168
|
+
[DOCS_ROUTE, `${DOCS_ROUTE}/:...slug`],
|
|
169
|
+
refuseDocsPath,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// 3. The site's docs-blog files must follow the setting: its docs source
|
|
173
|
+
// must take `DOCS_PATH` as its base.
|
|
174
|
+
if (docsPath !== DOCS_ROUTE) {
|
|
175
|
+
const sourcePath = join(process.cwd(), "lib/source.ts");
|
|
176
|
+
if (!existsSync(sourcePath)) {
|
|
177
|
+
refuseDocsPath(docsPath, "the site carries no docs-blog module");
|
|
178
|
+
}
|
|
179
|
+
if (!/\bbaseUrl:\s*DOCS_PATH\b/.test(readFileSync(sourcePath, "utf8"))) {
|
|
180
|
+
refuseDocsPath(
|
|
181
|
+
docsPath,
|
|
182
|
+
"this site's docs-blog files predate docsPath; re-install the docs-blog module's files",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return docsPath;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const docsPathAtBuild = readDocsPathFromSiteMdx();
|
|
190
|
+
|
|
191
|
+
// #10043 D4 — where the services live, and their categories. A site sets
|
|
192
|
+
// `services: { path: /capabilities }` in content/_site.mdx; it is read here,
|
|
193
|
+
// refused by name when it cannot be served, and baked into
|
|
194
|
+
// NEXT_PUBLIC_AILK_SERVICES_PATH for the middleware, the page renderer and
|
|
195
|
+
// every services-URL reader (lib/services-path.ts). Absent is `/services`.
|
|
196
|
+
// A Service's `parent` (its category) is checked here too, for the default
|
|
197
|
+
// locale's MDX files: it must name a Service with no parent of its own, and
|
|
198
|
+
// the site must carry the nested route.
|
|
199
|
+
|
|
200
|
+
function refuseServicesPath(value, reason) {
|
|
201
|
+
throw new Error(`content/_site.mdx services.path ${value}: ${reason}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function readServicesPathFromSiteMdx() {
|
|
205
|
+
const value = siteFrontmatterAtBuild?.services?.path;
|
|
206
|
+
if (value === undefined || value === null) return SERVICES_ROUTE;
|
|
207
|
+
|
|
208
|
+
const parsed = ServicesPathSchema.safeParse(value);
|
|
209
|
+
if (!parsed.success) {
|
|
210
|
+
refuseServicesPath(
|
|
211
|
+
value,
|
|
212
|
+
"must be one to four lowercase kebab segments with a leading slash and no trailing slash, not starting with api, og or v1, and not under /services",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const servicesPath = parsed.data;
|
|
216
|
+
refuseUnmountablePath(
|
|
217
|
+
servicesPath,
|
|
218
|
+
[SERVICES_ROUTE, `${SERVICES_ROUTE}/:slug`, `${SERVICES_ROUTE}/:slug/:child`],
|
|
219
|
+
refuseServicesPath,
|
|
220
|
+
);
|
|
221
|
+
// Two moved mounts must not overlap, in either direction.
|
|
222
|
+
const overlaps = (a, b) => a === b || a.startsWith(`${b}/`);
|
|
223
|
+
if (overlaps(servicesPath, docsPathAtBuild) || overlaps(docsPathAtBuild, servicesPath)) {
|
|
224
|
+
refuseServicesPath(servicesPath, `it overlaps docsPath ${docsPathAtBuild}`);
|
|
225
|
+
}
|
|
226
|
+
return servicesPath;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function refuseServiceParent(file, value, reason) {
|
|
230
|
+
throw new Error(`${file} parent ${value}: ${reason}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function readServiceParents() {
|
|
234
|
+
// Under the database adapter the pages are not on disk; the nested route
|
|
235
|
+
// answers 404 for a bad parent at request time instead.
|
|
236
|
+
if (process.env.CONTENT_ADAPTER === "database") return;
|
|
237
|
+
const dirs = [`${defaultLocaleAtBuild}/Service`, "Service"];
|
|
238
|
+
const nestedRoute = join(process.cwd(), "app/[locale]/services/[slug]/[child]/page.tsx");
|
|
239
|
+
for (const dir of dirs) {
|
|
240
|
+
let files;
|
|
241
|
+
try {
|
|
242
|
+
files = readdirSync(join(contentRootAtBuild, dir)).filter((f) => f.endsWith(".mdx"));
|
|
243
|
+
} catch {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const pages = files.map((file) => {
|
|
247
|
+
const frontmatter = readFrontmatter(join(contentRootAtBuild, dir, file)) ?? {};
|
|
248
|
+
return {
|
|
249
|
+
file: `content/${dir}/${file}`,
|
|
250
|
+
slug: frontmatter.slug ?? file.replace(/\.mdx$/, ""),
|
|
251
|
+
parent: frontmatter.parent,
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
const bySlug = new Map(pages.map((page) => [page.slug, page]));
|
|
255
|
+
for (const page of pages) {
|
|
256
|
+
if (page.parent === undefined || page.parent === null) continue;
|
|
257
|
+
const category = bySlug.get(page.parent);
|
|
258
|
+
if (category === undefined) {
|
|
259
|
+
refuseServiceParent(page.file, page.parent, `it names no Service in content/${dir}`);
|
|
260
|
+
}
|
|
261
|
+
if (category.parent !== undefined && category.parent !== null) {
|
|
262
|
+
refuseServiceParent(
|
|
263
|
+
page.file,
|
|
264
|
+
page.parent,
|
|
265
|
+
"that Service has its own parent; services take one category level",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (!existsSync(nestedRoute)) {
|
|
269
|
+
refuseServiceParent(
|
|
270
|
+
page.file,
|
|
271
|
+
page.parent,
|
|
272
|
+
"this site's services routes predate service categories; run create-ailk sync-routes",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const servicesPathAtBuild = readServicesPathFromSiteMdx();
|
|
280
|
+
readServiceParents();
|
|
281
|
+
|
|
84
282
|
/** @type {import('next').NextConfig} */
|
|
85
283
|
const nextConfig = {
|
|
86
284
|
reactStrictMode: true,
|
|
@@ -105,6 +303,10 @@ const nextConfig = {
|
|
|
105
303
|
// output shipped without them and postbuild's assert-content-bundled failed.
|
|
106
304
|
outputFileTracingIncludes: {
|
|
107
305
|
"/**": ["../../content/**/*.mdx"],
|
|
306
|
+
// #10041 D8 — /sitemap.xml and /llms.txt list the docs by reading this
|
|
307
|
+
// app's own content/<locale>/docs/ at request time.
|
|
308
|
+
"/sitemap.xml": ["./content/**/*.mdx"],
|
|
309
|
+
"/llms.txt": ["./content/**/*.mdx"],
|
|
108
310
|
},
|
|
109
311
|
// Workspace packages live outside this app's directory. Next.js must
|
|
110
312
|
// transpile them from source so that "use client" / "use server" directives
|
|
@@ -161,6 +363,13 @@ const nextConfig = {
|
|
|
161
363
|
// onto the hub's. Part of the source, not an operator setting: a build
|
|
162
364
|
// that carries the relay always carries the flag, and no other build does.
|
|
163
365
|
NEXT_PUBLIC_AILK_SAME_ORIGIN_RELAY: "1",
|
|
366
|
+
// The docs' public path (#10041), from content/_site.mdx's docsPath, or
|
|
367
|
+
// `/docs`. Read by middleware.ts and lib/docs-path.ts.
|
|
368
|
+
NEXT_PUBLIC_AILK_DOCS_PATH: docsPathAtBuild,
|
|
369
|
+
// The services' public base (#10043), from content/_site.mdx's
|
|
370
|
+
// services.path, or `/services`. Read by middleware.ts,
|
|
371
|
+
// lib/services-path.ts and @working-theory/page-renderer.
|
|
372
|
+
NEXT_PUBLIC_AILK_SERVICES_PATH: servicesPathAtBuild,
|
|
164
373
|
},
|
|
165
374
|
async headers() {
|
|
166
375
|
if (!STEALTH) return [];
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"dev": "next dev --webpack",
|
|
9
|
-
"build": "next build --webpack",
|
|
9
|
+
"build": "tsx scripts/check-nav-labels.ts && next build --webpack",
|
|
10
10
|
"postbuild": "tsx scripts/assert-content-bundled.ts",
|
|
11
11
|
"start": "next start",
|
|
12
12
|
"type-check": "tsc --noEmit",
|