create-ailk 0.27.0 → 0.28.1
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/chrome-readers.d.ts +21 -5
- package/dist/lib/chrome-readers.js +128 -20
- package/dist/lib/declared-env.d.ts +6 -0
- package/dist/lib/declared-env.js +2 -0
- package/dist/lib/env-catalog.js +20 -0
- package/dist/lib/import-graph.d.ts +16 -0
- package/dist/lib/import-graph.js +53 -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/template-record.d.ts +13 -4
- package/dist/template-record.js +8 -4
- package/dist/topology.d.ts +2 -1
- package/dist/topology.js +4 -2
- package/dist/update.d.ts +34 -2
- package/dist/update.js +395 -48
- 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 +224 -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 +237 -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 +279 -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/templates/tools/declared-env.ts +7 -0
- package/versions.json +48 -48
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The services routes under a moved base and with categories (#10043 D7/D8,
|
|
3
|
+
* AC-6), and unchanged on a site that sets nothing (AC-10).
|
|
4
|
+
*
|
|
5
|
+
* The moved-base cases load the routes afresh under `jest.isolateModules`
|
|
6
|
+
* with `NEXT_PUBLIC_AILK_SERVICES_PATH=/capabilities` (`lib/services-path.ts`
|
|
7
|
+
* reads it at module load) and read what each route hands `renderPage`; the
|
|
8
|
+
* no-setting cases render the routes as imported and compare their markup
|
|
9
|
+
* with a direct `renderPage` call over the same fixture, given today's input
|
|
10
|
+
* (the `pricing/__tests__/unconnected.test.tsx` equality-oracle shape).
|
|
11
|
+
*/
|
|
12
|
+
import { render } from "@testing-library/react";
|
|
13
|
+
import type { RenderPageInput } from "@working-theory/page-renderer";
|
|
14
|
+
import type { ReactElement } from "react";
|
|
15
|
+
|
|
16
|
+
jest.mock("next/navigation", () => ({
|
|
17
|
+
__esModule: true,
|
|
18
|
+
notFound: jest.fn(() => {
|
|
19
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
20
|
+
}),
|
|
21
|
+
permanentRedirect: jest.fn(() => {
|
|
22
|
+
throw new Error("NEXT_REDIRECT");
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
jest.mock("@working-theory/page-renderer", () => {
|
|
27
|
+
const actual = jest.requireActual("@working-theory/page-renderer");
|
|
28
|
+
return {
|
|
29
|
+
...actual,
|
|
30
|
+
renderPage: jest.fn(actual.renderPage),
|
|
31
|
+
getMdxSlugs: jest.fn(actual.getMdxSlugs),
|
|
32
|
+
getPageSections: jest.fn(actual.getPageSections),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
import ServicesPage from "../page";
|
|
37
|
+
import ServiceSlugPage from "../[slug]/page";
|
|
38
|
+
|
|
39
|
+
const actual = jest.requireActual("@working-theory/page-renderer") as {
|
|
40
|
+
renderPage: (input: RenderPageInput) => Promise<ReactElement>;
|
|
41
|
+
getMdxSlugs: (...args: unknown[]) => Promise<string[]>;
|
|
42
|
+
getPageSections: (...args: unknown[]) => Promise<unknown[]>;
|
|
43
|
+
};
|
|
44
|
+
const actualRenderPage = actual.renderPage;
|
|
45
|
+
const mocked = jest.requireMock("@working-theory/page-renderer") as {
|
|
46
|
+
renderPage: jest.Mock;
|
|
47
|
+
getMdxSlugs: jest.Mock;
|
|
48
|
+
getPageSections: jest.Mock;
|
|
49
|
+
};
|
|
50
|
+
const nav = jest.requireMock("next/navigation") as {
|
|
51
|
+
notFound: jest.Mock;
|
|
52
|
+
permanentRedirect: jest.Mock;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Every case starts from the real collaborators, with fresh call records.
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
mocked.renderPage.mockReset().mockImplementation(actual.renderPage);
|
|
58
|
+
mocked.getMdxSlugs.mockReset().mockImplementation(actual.getMdxSlugs);
|
|
59
|
+
mocked.getPageSections.mockReset().mockImplementation(actual.getPageSections);
|
|
60
|
+
nav.notFound.mockClear();
|
|
61
|
+
nav.permanentRedirect.mockClear();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
type Frontmatter = Record<string, unknown>;
|
|
65
|
+
type Source = { frontmatter: Frontmatter; body: string };
|
|
66
|
+
|
|
67
|
+
function service(slug: string, extra: Frontmatter = {}): Source {
|
|
68
|
+
return {
|
|
69
|
+
frontmatter: {
|
|
70
|
+
type: "Service",
|
|
71
|
+
slug,
|
|
72
|
+
title: slug.toUpperCase(),
|
|
73
|
+
description: `${slug} description.`,
|
|
74
|
+
...extra,
|
|
75
|
+
},
|
|
76
|
+
body: `# ${slug}\n\nBody.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A `getMdxSource` over a slug → source table; anything else is null. */
|
|
81
|
+
function sources(table: Record<string, Source>) {
|
|
82
|
+
return jest.fn(async (_type: string, slug: string) => table[slug] ?? null);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The moved routes resolve to what the stubbed `renderPage` returns — the
|
|
86
|
+
// captured `{ input }` — so they are typed as resolving to `unknown`.
|
|
87
|
+
interface Routes {
|
|
88
|
+
hub: (props: Parameters<typeof ServicesPage>[0]) => Promise<unknown>;
|
|
89
|
+
slugPage: (props: Parameters<typeof ServiceSlugPage>[0]) => Promise<unknown>;
|
|
90
|
+
childPage: (props: {
|
|
91
|
+
params: Promise<{ locale: string; slug: string; child: string }>;
|
|
92
|
+
getMdxSource?: unknown;
|
|
93
|
+
}) => Promise<unknown>;
|
|
94
|
+
renderPage: jest.Mock;
|
|
95
|
+
getMdxSlugs: jest.Mock;
|
|
96
|
+
getPageSections: jest.Mock;
|
|
97
|
+
notFound: jest.Mock;
|
|
98
|
+
permanentRedirect: jest.Mock;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The three routes and their mocked collaborators, loaded with a services base. */
|
|
102
|
+
function loadMoved(base: string): Routes {
|
|
103
|
+
const saved = process.env.NEXT_PUBLIC_AILK_SERVICES_PATH;
|
|
104
|
+
process.env.NEXT_PUBLIC_AILK_SERVICES_PATH = base;
|
|
105
|
+
let routes: Routes | undefined;
|
|
106
|
+
try {
|
|
107
|
+
// The mocked collaborators are shared with the top-level registry; only
|
|
108
|
+
// the routes and `lib/services-path.ts` load afresh with the base.
|
|
109
|
+
mocked.renderPage.mockImplementation(async (input: RenderPageInput) => ({
|
|
110
|
+
input,
|
|
111
|
+
}));
|
|
112
|
+
jest.isolateModules(() => {
|
|
113
|
+
routes = {
|
|
114
|
+
hub: require("../page").default,
|
|
115
|
+
slugPage: require("../[slug]/page").default,
|
|
116
|
+
childPage: require("../[slug]/[child]/page").default,
|
|
117
|
+
renderPage: mocked.renderPage,
|
|
118
|
+
getMdxSlugs: mocked.getMdxSlugs,
|
|
119
|
+
getPageSections: mocked.getPageSections,
|
|
120
|
+
notFound: nav.notFound,
|
|
121
|
+
permanentRedirect: nav.permanentRedirect,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
} finally {
|
|
125
|
+
if (saved === undefined) delete process.env.NEXT_PUBLIC_AILK_SERVICES_PATH;
|
|
126
|
+
else process.env.NEXT_PUBLIC_AILK_SERVICES_PATH = saved;
|
|
127
|
+
}
|
|
128
|
+
return routes!;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const params = (locale: string, slug: string) =>
|
|
132
|
+
Promise.resolve({ locale, slug });
|
|
133
|
+
|
|
134
|
+
describe("services under /capabilities (#10043 AC-6)", () => {
|
|
135
|
+
let r: Routes;
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
r = loadMoved("/capabilities");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("redirects a nested service's flat URL to its category URL, per locale", async () => {
|
|
141
|
+
const load = sources({ x: service("x", { parent: "ai" }) });
|
|
142
|
+
await expect(
|
|
143
|
+
r.slugPage({ params: params("en", "x"), getMdxSource: load as never }),
|
|
144
|
+
).rejects.toThrow("NEXT_REDIRECT");
|
|
145
|
+
expect(r.permanentRedirect).toHaveBeenLastCalledWith("/capabilities/ai/x");
|
|
146
|
+
await expect(
|
|
147
|
+
r.slugPage({ params: params("pt-BR", "x"), getMdxSource: load as never }),
|
|
148
|
+
).rejects.toThrow("NEXT_REDIRECT");
|
|
149
|
+
expect(r.permanentRedirect).toHaveBeenLastCalledWith(
|
|
150
|
+
"/pt-BR/capabilities/ai/x",
|
|
151
|
+
);
|
|
152
|
+
expect(r.renderPage).not.toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("lists a category's services after its body, in slug order", async () => {
|
|
156
|
+
const load = sources({
|
|
157
|
+
ai: service("ai"),
|
|
158
|
+
a: service("a", { parent: "ai", summary: "A summary." }),
|
|
159
|
+
b: service("b", { parent: "ai" }),
|
|
160
|
+
c: service("c", { parent: "other" }),
|
|
161
|
+
});
|
|
162
|
+
r.getMdxSlugs.mockResolvedValue(["c", "b", "ai", "a"]);
|
|
163
|
+
const out = (await r.slugPage({
|
|
164
|
+
params: params("en", "ai"),
|
|
165
|
+
getMdxSource: load as never,
|
|
166
|
+
})) as {
|
|
167
|
+
input: RenderPageInput & {
|
|
168
|
+
trailing: ReactElement<{
|
|
169
|
+
sections: Array<{ data: { headline: string; items: unknown[] } }>;
|
|
170
|
+
}>;
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
expect(out.input).toMatchObject({
|
|
174
|
+
type: "Service",
|
|
175
|
+
slug: "ai",
|
|
176
|
+
bodyMode: "mdx-body",
|
|
177
|
+
});
|
|
178
|
+
const [list] = out.input.trailing.props.sections;
|
|
179
|
+
expect(list?.data.headline).toBe("Services in this area");
|
|
180
|
+
expect(list?.data.items).toEqual([
|
|
181
|
+
{
|
|
182
|
+
title: "A",
|
|
183
|
+
description: "A summary.",
|
|
184
|
+
link: { label: "View service", href: "/capabilities/ai/a" },
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
title: "B",
|
|
188
|
+
description: "b description.",
|
|
189
|
+
link: { label: "View service", href: "/capabilities/ai/b" },
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("renders a service with no children through today's input", async () => {
|
|
195
|
+
const load = sources({ solo: service("solo") });
|
|
196
|
+
r.getMdxSlugs.mockResolvedValue(["solo"]);
|
|
197
|
+
const out = (await r.slugPage({
|
|
198
|
+
params: params("en", "solo"),
|
|
199
|
+
getMdxSource: load as never,
|
|
200
|
+
})) as {
|
|
201
|
+
input: Record<string, unknown>;
|
|
202
|
+
};
|
|
203
|
+
expect(out.input).toEqual({
|
|
204
|
+
type: "Service",
|
|
205
|
+
slug: "solo",
|
|
206
|
+
locale: "en",
|
|
207
|
+
bodyMode: "mdx-body",
|
|
208
|
+
populatedDescriptionTestId: "service-description",
|
|
209
|
+
getMdxSource: load,
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("serves a nested service through renderPage at its category", async () => {
|
|
214
|
+
const load = sources({
|
|
215
|
+
ai: service("ai"),
|
|
216
|
+
x: service("x", { parent: "ai" }),
|
|
217
|
+
});
|
|
218
|
+
const out = (await r.childPage({
|
|
219
|
+
params: Promise.resolve({ locale: "en", slug: "ai", child: "x" }),
|
|
220
|
+
getMdxSource: load,
|
|
221
|
+
})) as { input: Record<string, unknown> };
|
|
222
|
+
expect(out.input).toMatchObject({
|
|
223
|
+
type: "Service",
|
|
224
|
+
slug: "x",
|
|
225
|
+
locale: "en",
|
|
226
|
+
bodyMode: "mdx-body",
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it.each<[string, Record<string, Source>]>([
|
|
231
|
+
[
|
|
232
|
+
"its parent is another category",
|
|
233
|
+
{ ai: service("ai"), x: service("x", { parent: "other" }) },
|
|
234
|
+
],
|
|
235
|
+
["its category page is missing", { x: service("x", { parent: "ai" }) }],
|
|
236
|
+
[
|
|
237
|
+
"its category has its own parent",
|
|
238
|
+
{
|
|
239
|
+
top: service("top"),
|
|
240
|
+
ai: service("ai", { parent: "top" }),
|
|
241
|
+
x: service("x", { parent: "ai" }),
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
])("answers 404 for a nested URL when %s", async (_label, table) => {
|
|
245
|
+
await expect(
|
|
246
|
+
r.childPage({
|
|
247
|
+
params: Promise.resolve({ locale: "en", slug: "ai", child: "x" }),
|
|
248
|
+
getMdxSource: sources(table),
|
|
249
|
+
}),
|
|
250
|
+
).rejects.toThrow("NEXT_NOT_FOUND");
|
|
251
|
+
expect(r.renderPage).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("hands a missing nested service to renderPage", async () => {
|
|
255
|
+
const out = (await r.childPage({
|
|
256
|
+
params: Promise.resolve({ locale: "en", slug: "ai", child: "x" }),
|
|
257
|
+
getMdxSource: sources({}),
|
|
258
|
+
})) as { input: Record<string, unknown> };
|
|
259
|
+
expect(out.input).toMatchObject({ type: "Service", slug: "x" });
|
|
260
|
+
expect(r.notFound).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("re-points the hub's section links onto the base", async () => {
|
|
264
|
+
const hubSource = {
|
|
265
|
+
frontmatter: {
|
|
266
|
+
type: "ServicesList",
|
|
267
|
+
slug: "services",
|
|
268
|
+
title: "Services",
|
|
269
|
+
description: "All.",
|
|
270
|
+
sections: [{ type: "hero", id: "h", data: {} }],
|
|
271
|
+
},
|
|
272
|
+
body: "",
|
|
273
|
+
};
|
|
274
|
+
// No Service pages: this case pins the remap of the hub's own links;
|
|
275
|
+
// the generated child cards are hub-children.test.tsx's (#10121).
|
|
276
|
+
r.getMdxSlugs.mockResolvedValue([]);
|
|
277
|
+
r.getPageSections.mockResolvedValue([
|
|
278
|
+
{
|
|
279
|
+
type: "items",
|
|
280
|
+
id: "list",
|
|
281
|
+
data: {
|
|
282
|
+
items: [
|
|
283
|
+
{ title: "X", link: { label: "Go", href: "/services/x" } },
|
|
284
|
+
{
|
|
285
|
+
title: "Y",
|
|
286
|
+
link: { label: "Go", href: "https://example.com/services" },
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
]);
|
|
292
|
+
const out = (await r.hub({
|
|
293
|
+
params: Promise.resolve({ locale: "en" }),
|
|
294
|
+
getMdxSource: jest.fn().mockResolvedValue(hubSource),
|
|
295
|
+
})) as { input: RenderPageInput };
|
|
296
|
+
const sections = (await out.input.getPageSections!(
|
|
297
|
+
hubSource.frontmatter as never,
|
|
298
|
+
)) as Array<{
|
|
299
|
+
data: { items: Array<{ link: { href: string } }> };
|
|
300
|
+
}>;
|
|
301
|
+
expect(sections[0]?.data.items.map((item) => item.link.href)).toEqual([
|
|
302
|
+
"/capabilities/x",
|
|
303
|
+
"https://example.com/services",
|
|
304
|
+
]);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("a site that sets nothing (#10043 AC-10)", () => {
|
|
309
|
+
beforeEach(() => {
|
|
310
|
+
mocked.getMdxSlugs.mockResolvedValue(["consulting", "implementation"]);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("renders the /services hub exactly as a direct renderPage call", async () => {
|
|
314
|
+
// No Service pages: the hub's generated child cards (#10121) are pinned in
|
|
315
|
+
// hub-children.test.tsx; this case pins "no services.path changes nothing".
|
|
316
|
+
mocked.getMdxSlugs.mockResolvedValue([]);
|
|
317
|
+
const load = jest.fn().mockResolvedValue({
|
|
318
|
+
frontmatter: {
|
|
319
|
+
type: "ServicesList",
|
|
320
|
+
slug: "services",
|
|
321
|
+
title: "Services",
|
|
322
|
+
description: "All services.",
|
|
323
|
+
},
|
|
324
|
+
body: "# Services\n\nBody.",
|
|
325
|
+
});
|
|
326
|
+
const viaRoute = render(
|
|
327
|
+
await ServicesPage({
|
|
328
|
+
params: Promise.resolve({ locale: "en" }),
|
|
329
|
+
getMdxSource: load,
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
const direct = render(
|
|
333
|
+
await actualRenderPage({
|
|
334
|
+
type: "ServicesList",
|
|
335
|
+
slug: "services",
|
|
336
|
+
locale: "en",
|
|
337
|
+
translationsNamespace: "services",
|
|
338
|
+
bodyMode: "mdx-body",
|
|
339
|
+
getMdxSource: load,
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
expect(viaRoute.container.innerHTML).toBe(direct.container.innerHTML);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("renders /services/consulting exactly as a direct renderPage call", async () => {
|
|
346
|
+
const load = sources({
|
|
347
|
+
consulting: service("consulting"),
|
|
348
|
+
implementation: service("implementation"),
|
|
349
|
+
});
|
|
350
|
+
const viaRoute = render(
|
|
351
|
+
await ServiceSlugPage({
|
|
352
|
+
params: params("en", "consulting"),
|
|
353
|
+
getMdxSource: load as never,
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
const direct = render(
|
|
357
|
+
await actualRenderPage({
|
|
358
|
+
type: "Service",
|
|
359
|
+
slug: "consulting",
|
|
360
|
+
locale: "en",
|
|
361
|
+
bodyMode: "mdx-body",
|
|
362
|
+
populatedDescriptionTestId: "service-description",
|
|
363
|
+
getMdxSource: load as never,
|
|
364
|
+
}),
|
|
365
|
+
);
|
|
366
|
+
expect(viaRoute.container.innerHTML).toBe(direct.container.innerHTML);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
// Services list page. Reads ServicesList MDX via the content adapter and emits
|
|
2
|
-
// a JSON-LD CollectionPage via the dispatcher.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
// a JSON-LD CollectionPage via the dispatcher. The /for composed pattern
|
|
3
|
+
// (#7745 D2): sections when the frontmatter carries `sections[]`, MDX prose
|
|
4
|
+
// otherwise.
|
|
5
|
+
// It lists every top-level Service — a category, or a service with no
|
|
6
|
+
// category — through one generated card section (#10121,
|
|
7
|
+
// apps/web/lib/hub-children.tsx). A service with a `parent` is listed on its
|
|
8
|
+
// category's page instead (#10043 D7). Cards link to the service's public URL
|
|
9
|
+
// under the site's services base, and on a site that moved its services the
|
|
10
|
+
// hub's own section links name that base too (#10043 D8).
|
|
11
|
+
import type {
|
|
12
|
+
getMdxSlugs as defaultGetMdxSlugs,
|
|
13
|
+
getMdxSource as defaultGetMdxSource,
|
|
14
|
+
getPageSections as defaultGetPageSections,
|
|
8
15
|
} from "@working-theory/page-renderer";
|
|
16
|
+
import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
|
|
9
17
|
import type { Metadata } from "next";
|
|
18
|
+
import { getTranslations } from "next-intl/server";
|
|
10
19
|
import type { ReactElement } from "react";
|
|
11
20
|
|
|
21
|
+
import { type HubConfig, hubPageInput } from "../../../lib/hub-children";
|
|
22
|
+
import {
|
|
23
|
+
serviceChildrenSection,
|
|
24
|
+
withPublicServicesSections,
|
|
25
|
+
} from "../../../lib/service-listing";
|
|
26
|
+
|
|
12
27
|
interface Props {
|
|
13
28
|
params: Promise<{ locale: string }>;
|
|
14
29
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
30
|
+
getPageSections?: typeof defaultGetPageSections;
|
|
31
|
+
getMdxSlugs?: typeof defaultGetMdxSlugs;
|
|
15
32
|
}
|
|
16
33
|
|
|
17
34
|
export async function generateMetadata({
|
|
@@ -23,23 +40,32 @@ export async function generateMetadata({
|
|
|
23
40
|
|
|
24
41
|
export default async function Page({
|
|
25
42
|
params,
|
|
26
|
-
|
|
43
|
+
...loaders
|
|
27
44
|
}: Props): Promise<ReactElement> {
|
|
28
45
|
const { locale } = await params;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
const t = await getTranslations({ namespace: "services", locale });
|
|
47
|
+
const labels = {
|
|
48
|
+
childrenHeading: t("childrenHeading"),
|
|
49
|
+
viewService: t("viewService"),
|
|
50
|
+
};
|
|
51
|
+
const hub: HubConfig = {
|
|
52
|
+
listType: "ServicesList",
|
|
53
|
+
slug: "services",
|
|
54
|
+
childType: "Service",
|
|
55
|
+
linkLabel: labels.viewService,
|
|
56
|
+
sectionId: "service-cards",
|
|
57
|
+
body: "composed",
|
|
58
|
+
// No category: the same reading the category listing gives `parent`
|
|
59
|
+
// (absent, blank or not a string), so every service is listed somewhere.
|
|
60
|
+
includeChild: ({ frontmatter: { parent } }) =>
|
|
61
|
+
typeof parent !== "string" || parent.length === 0,
|
|
62
|
+
// The category page's own card shape, at the service's public URL.
|
|
63
|
+
card: (page) => serviceChildrenSection([page], labels).data.items[0]!,
|
|
64
|
+
mapSections: withPublicServicesSections,
|
|
65
|
+
};
|
|
37
66
|
return renderPage({
|
|
38
67
|
type: "ServicesList",
|
|
39
68
|
slug: "services",
|
|
40
|
-
locale,
|
|
41
|
-
translationsNamespace: "services",
|
|
42
|
-
...(composed ? {} : { bodyMode: "mdx-body" as const }),
|
|
43
|
-
...(getMdxSource !== undefined && { getMdxSource }),
|
|
69
|
+
...(await hubPageInput(hub, locale, loaders)),
|
|
44
70
|
});
|
|
45
71
|
}
|
|
@@ -61,7 +61,7 @@ describe("software/[slug] — snapshot regression", () => {
|
|
|
61
61
|
});
|
|
62
62
|
const { container } = render(element);
|
|
63
63
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
64
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"@working-theory/page-renderer","description":"Single rendering primitive for MDX-wired routes.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"@working-theory/page-renderer","description":"Single rendering primitive for MDX-wired routes."}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><
|
|
64
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"@working-theory/page-renderer","description":"Single rendering primitive for MDX-wired routes.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"@working-theory/page-renderer","description":"Single rendering primitive for MDX-wired routes."}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-text-muted) text-body-sm leading-relaxed [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">## Overview
|
|
65
65
|
|
|
66
66
|
Body content.</div></div></article></div></div></div></main>"
|
|
67
67
|
`);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub child cards (#10121) for /team. The shared cases live in
|
|
3
|
+
* `lib/__tests__/hub-children.fixtures.ts`; this file sits beside the route so
|
|
4
|
+
* a scaffold without /team drops it with the route (#10157).
|
|
5
|
+
*/
|
|
6
|
+
const mockRenderPage = jest.fn(async (..._args: unknown[]) => "<rendered/>");
|
|
7
|
+
|
|
8
|
+
jest.mock("@working-theory/page-renderer", () => ({
|
|
9
|
+
__esModule: true,
|
|
10
|
+
renderPage: (...args: unknown[]) => mockRenderPage(...args),
|
|
11
|
+
renderPageMetadata: jest.fn(),
|
|
12
|
+
// Never reached — every call in this suite passes its loaders via DI.
|
|
13
|
+
getMdxSlugs: jest.fn(),
|
|
14
|
+
getMdxSource: jest.fn(),
|
|
15
|
+
getPageSections: jest.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
jest.mock("@working-theory/ui/sections", () => ({
|
|
19
|
+
__esModule: true,
|
|
20
|
+
SectionRenderer: () => null,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import TeamPage from "../page";
|
|
24
|
+
import {
|
|
25
|
+
describeHub,
|
|
26
|
+
type HubPage,
|
|
27
|
+
} from "../../../../lib/__tests__/hub-children.fixtures";
|
|
28
|
+
|
|
29
|
+
describeHub(
|
|
30
|
+
["team", TeamPage as HubPage, "TeamList", "TeamMember", "composed"],
|
|
31
|
+
mockRenderPage,
|
|
32
|
+
);
|
|
@@ -1,49 +1,50 @@
|
|
|
1
1
|
// Team list page (#7744 D2). Reads TeamList MDX via the content adapter and emits
|
|
2
|
-
// a JSON-LD CollectionPage via the dispatcher.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
import type {
|
|
7
|
-
|
|
8
|
-
getMdxSource as
|
|
9
|
-
|
|
10
|
-
renderPageMetadata,
|
|
2
|
+
// a JSON-LD CollectionPage via the dispatcher. The /for composed pattern:
|
|
3
|
+
// sections when the frontmatter carries `sections[]`, MDX prose otherwise.
|
|
4
|
+
// It lists every TeamMember page through one generated card section (#10121,
|
|
5
|
+
// apps/web/lib/hub-children.tsx — the /alternatives and /templates shape).
|
|
6
|
+
import type {
|
|
7
|
+
getMdxSlugs as defaultGetMdxSlugs,
|
|
8
|
+
getMdxSource as defaultGetMdxSource,
|
|
9
|
+
getPageSections as defaultGetPageSections,
|
|
11
10
|
} from "@working-theory/page-renderer";
|
|
11
|
+
import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
|
|
12
12
|
import type { Metadata } from "next";
|
|
13
13
|
import type { ReactElement } from "react";
|
|
14
14
|
|
|
15
|
+
import { type HubConfig, hubPageInput } from "../../../lib/hub-children";
|
|
16
|
+
|
|
15
17
|
interface Props {
|
|
16
18
|
params: Promise<{ locale: string }>;
|
|
17
19
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
20
|
+
getPageSections?: typeof defaultGetPageSections;
|
|
21
|
+
getMdxSlugs?: typeof defaultGetMdxSlugs;
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
const HUB: HubConfig = {
|
|
25
|
+
listType: "TeamList",
|
|
26
|
+
slug: "team",
|
|
27
|
+
childType: "TeamMember",
|
|
28
|
+
linkLabel: "View profile",
|
|
29
|
+
sectionId: "team-member-cards",
|
|
30
|
+
body: "composed",
|
|
31
|
+
};
|
|
32
|
+
|
|
20
33
|
export async function generateMetadata({
|
|
21
34
|
params,
|
|
22
35
|
}: Pick<Props, "params">): Promise<Metadata> {
|
|
23
36
|
const { locale } = await params;
|
|
24
|
-
return renderPageMetadata({
|
|
25
|
-
type: "TeamList",
|
|
26
|
-
slug: "team",
|
|
27
|
-
locale,
|
|
28
|
-
});
|
|
37
|
+
return renderPageMetadata({ type: "TeamList", slug: "team", locale });
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
export default async function Page({
|
|
32
41
|
params,
|
|
33
|
-
|
|
42
|
+
...loaders
|
|
34
43
|
}: Props): Promise<ReactElement> {
|
|
35
44
|
const { locale } = await params;
|
|
36
|
-
const source = await (getMdxSource ?? loadMdxSource)("TeamList", "team", locale);
|
|
37
|
-
const composed =
|
|
38
|
-
source !== null &&
|
|
39
|
-
Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
|
|
40
|
-
((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) > 0;
|
|
41
45
|
return renderPage({
|
|
42
46
|
type: "TeamList",
|
|
43
47
|
slug: "team",
|
|
44
|
-
locale,
|
|
45
|
-
translationsNamespace: "team",
|
|
46
|
-
...(composed ? {} : { bodyMode: "mdx-body" as const }),
|
|
47
|
-
...(getMdxSource !== undefined && { getMdxSource }),
|
|
48
|
+
...(await hubPageInput(HUB, locale, loaders)),
|
|
48
49
|
});
|
|
49
50
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// TechArticle detail page. Reads the TechArticle MDX for the given slug, renders via
|
|
2
2
|
// @working-theory/page-renderer, and emits a JSON-LD <script>. Unknown slugs trigger 404.
|
|
3
3
|
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getMdxSource as loadMdxSource,
|
|
6
|
+
renderPage,
|
|
7
|
+
renderPageMetadata,
|
|
8
|
+
} from "@working-theory/page-renderer";
|
|
5
9
|
import type { Metadata } from "next";
|
|
6
10
|
import type { ReactElement } from "react";
|
|
7
11
|
|
|
@@ -22,12 +26,29 @@ export default async function Page({
|
|
|
22
26
|
getMdxSource,
|
|
23
27
|
}: Props): Promise<ReactElement> {
|
|
24
28
|
const { locale, slug } = await params;
|
|
29
|
+
// A page that composes `sections[]` renders them (JSON-LD is built from
|
|
30
|
+
// the same sections); a prose page renders its MDX body — the rule
|
|
31
|
+
// /vs/[slug] applies (#10122).
|
|
32
|
+
const source = await (getMdxSource ?? loadMdxSource)(
|
|
33
|
+
"TechArticle",
|
|
34
|
+
slug,
|
|
35
|
+
locale,
|
|
36
|
+
);
|
|
37
|
+
const composed =
|
|
38
|
+
source !== null &&
|
|
39
|
+
Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
|
|
40
|
+
((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) >
|
|
41
|
+
0;
|
|
25
42
|
return renderPage({
|
|
26
43
|
type: "TechArticle",
|
|
27
44
|
slug,
|
|
28
45
|
locale,
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
...(composed
|
|
47
|
+
? {}
|
|
48
|
+
: {
|
|
49
|
+
bodyMode: "mdx-body" as const,
|
|
50
|
+
populatedDescriptionTestId: "tech-article-description",
|
|
51
|
+
}),
|
|
31
52
|
...(getMdxSource !== undefined && { getMdxSource }),
|
|
32
53
|
});
|
|
33
54
|
}
|