create-ailk 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -9
- package/dist/add-module-core.js +2 -1
- package/dist/add-module.d.ts +2 -1
- package/dist/add-module.js +9 -4
- package/dist/add-route.js +46 -19
- package/dist/body-heading.d.ts +56 -0
- package/dist/body-heading.js +394 -0
- package/dist/cli.js +28 -1
- package/dist/content-payloads.js +2 -2
- package/dist/derive-architecture.js +8 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/lib/auth-secret-file.d.ts +6 -2
- package/dist/lib/auth-secret-file.js +46 -20
- package/dist/lib/env-catalog.js +20 -0
- package/dist/lib/site-modules.d.ts +8 -0
- package/dist/lib/site-modules.js +34 -0
- package/dist/module-architecture.js +25 -14
- package/dist/paid-sections.d.ts +17 -0
- package/dist/paid-sections.js +39 -0
- package/dist/programmatic.d.ts +16 -0
- package/dist/programmatic.js +73 -4
- package/dist/remove-module.d.ts +98 -0
- package/dist/remove-module.js +620 -0
- package/dist/seed-flows.d.ts +2 -2
- package/dist/seed-flows.js +16 -9
- package/dist/site-roster.js +4 -1
- package/dist/starter-grid.d.ts +26 -0
- package/dist/starter-grid.js +34 -0
- package/dist/surfaces.d.ts +26 -0
- package/dist/surfaces.js +112 -11
- package/dist/sync-routes.d.ts +7 -0
- package/dist/sync-routes.js +53 -6
- package/dist/template-pages.d.ts +36 -0
- package/dist/template-pages.js +129 -0
- package/dist/topology.d.ts +2 -1
- package/dist/topology.js +4 -2
- package/dist/update.d.ts +3 -1
- package/dist/update.js +90 -6
- package/package.json +3 -3
- package/release-scope.json +2 -1
- package/site-templates/aliases/hub.TEMPLATE-MANIFEST.json +115 -0
- package/templates/.env.example +12 -2
- package/templates/CLAUDE.md +20 -20
- package/templates/README.md +19 -19
- package/templates/apps/api/.env.example +16 -5
- package/templates/apps/api/CLAUDE.md +32 -11
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +37 -0
- package/templates/apps/api/src/vercel-handler.ts +8 -1
- package/templates/apps/api/vercel.json +7 -1
- package/templates/apps/mcp/CLAUDE.md +18 -10
- package/templates/apps/web/.env.example +27 -0
- package/templates/apps/web/CLAUDE.md +56 -8
- package/templates/apps/web/__tests__/setup.ts +7 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +7 -7
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/next-path.test.ts +24 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +51 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +13 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +10 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/page.test.tsx +197 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +23 -7
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +61 -2
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/connect/__tests__/page.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/(authed)/account/connect/page.tsx +152 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +76 -21
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +46 -16
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +180 -2
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +122 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/hub-children.test.tsx +65 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +23 -11
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/consent.actions.ts +44 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +269 -42
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +88 -30
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/courses/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/courses/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/events/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/events/page.tsx +23 -7
- package/templates/apps/web/app/[locale]/features/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +23 -25
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +95 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/save.actions.test.ts +193 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/email-moment.ts +92 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +10 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +513 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +122 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.no-commerce.test.tsx +195 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/landing.test.tsx +293 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +397 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/checkout-seam.ts +74 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +264 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/save.actions.ts +75 -0
- package/templates/apps/web/app/[locale]/for/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +5 -1
- package/templates/apps/web/app/[locale]/for/page.tsx +23 -25
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +25 -4
- package/templates/apps/web/app/[locale]/integrations/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/integrations/page.tsx +23 -11
- package/templates/apps/web/app/[locale]/layout.tsx +161 -128
- package/templates/apps/web/app/[locale]/page.tsx +51 -3
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +16 -3
- package/templates/apps/web/app/[locale]/partners/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/partners/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/portfolio/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +25 -24
- package/templates/apps/web/app/[locale]/pricing/[slug]/__tests__/page.test.tsx +321 -0
- package/templates/apps/web/app/[locale]/pricing/[slug]/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/pricing-pages.test.tsx +215 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +56 -341
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +21 -4
- package/templates/apps/web/app/[locale]/products/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/products/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/projects/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +18 -11
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +25 -4
- package/templates/apps/web/app/[locale]/resources/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/resources/page.tsx +20 -10
- package/templates/apps/web/app/[locale]/services/[slug]/[child]/page.tsx +68 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +64 -9
- package/templates/apps/web/app/[locale]/services/__tests__/hub-children.test.tsx +158 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/services/__tests__/service-detail-sections.test.tsx +223 -0
- package/templates/apps/web/app/[locale]/services/__tests__/service-families.test.tsx +368 -0
- package/templates/apps/web/app/[locale]/services/page.tsx +45 -19
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/team/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/team/page.tsx +25 -24
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +24 -3
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/templates/[slug]/__tests__/page.test.tsx +203 -0
- package/templates/apps/web/app/[locale]/templates/[slug]/page.tsx +81 -0
- package/templates/apps/web/app/[locale]/templates/__tests__/page.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/templates/page.tsx +108 -0
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +24 -3
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +322 -2
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +161 -29
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +51 -0
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +13 -0
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +233 -7
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +92 -41
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +77 -39
- package/templates/apps/web/app/llms.txt/route.ts +41 -3
- package/templates/apps/web/app/robots.txt/route.ts +5 -1
- package/templates/apps/web/app/sitemap.xml/route.ts +45 -3
- package/templates/apps/web/auth.ts +102 -50
- package/templates/apps/web/components/signed-in-nav.tsx +8 -1
- package/templates/apps/web/lib/__tests__/bot-check.test.ts +254 -0
- package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +212 -13
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +13 -1
- package/templates/apps/web/lib/__tests__/docs-index.test.ts +93 -0
- package/templates/apps/web/lib/__tests__/docs-path.test.ts +168 -0
- package/templates/apps/web/lib/__tests__/hub-children.fixtures.ts +278 -0
- package/templates/apps/web/lib/__tests__/nav-label-keys.test.ts +138 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +178 -2
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +183 -0
- package/templates/apps/web/lib/__tests__/page-chrome.test.ts +412 -0
- package/templates/apps/web/lib/__tests__/pricing-page.test.ts +142 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +484 -4
- package/templates/apps/web/lib/__tests__/route-family.test.ts +57 -0
- package/templates/apps/web/lib/__tests__/service-listing.test.ts +111 -0
- package/templates/apps/web/lib/__tests__/services-path.test.ts +96 -0
- package/templates/apps/web/lib/__tests__/site-header.test.ts +151 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +148 -6
- package/templates/apps/web/lib/__tests__/telemetry-scrub.test.ts +84 -0
- package/templates/apps/web/lib/__tests__/template-listing.test.ts +158 -0
- package/templates/apps/web/lib/auth-secret.ts +57 -0
- package/templates/apps/web/lib/bot-check.ts +138 -0
- package/templates/apps/web/lib/consent-visitor.ts +81 -7
- package/templates/apps/web/lib/deck-page.ts +6 -3
- package/templates/apps/web/lib/docs-index.ts +100 -0
- package/templates/apps/web/lib/docs-path.ts +94 -0
- package/templates/apps/web/lib/hub-children.tsx +318 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +22 -2
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +11 -8
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/nav-label-check.ts +61 -0
- package/templates/apps/web/lib/nav-label-keys.ts +78 -0
- package/templates/apps/web/lib/newsletter.actions.ts +31 -6
- package/templates/apps/web/lib/offerings-pricing.ts +135 -6
- package/templates/apps/web/lib/page-chrome.ts +367 -0
- package/templates/apps/web/lib/posting-title.ts +26 -4
- package/templates/apps/web/lib/pricing-page.ts +430 -0
- package/templates/apps/web/lib/relay.ts +44 -4
- package/templates/apps/web/lib/route-family.ts +69 -0
- package/templates/apps/web/lib/service-listing.ts +89 -0
- package/templates/apps/web/lib/services-path.ts +97 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +17 -6
- package/templates/apps/web/lib/site-header.ts +94 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +68 -3
- package/templates/apps/web/lib/template-listing.ts +169 -0
- package/templates/apps/web/messages/en.json +144 -3
- package/templates/apps/web/middleware.ts +179 -12
- package/templates/apps/web/next.config.mjs +220 -3
- package/templates/apps/web/package.json +3 -2
- package/templates/apps/web/scripts/assert-content-bundled.ts +28 -6
- package/templates/apps/web/scripts/check-nav-labels.ts +39 -0
- package/templates/apps/web/sentry.server.config.ts +30 -2
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -9
- package/templates/apps/web/src/lib/rate-limit.ts +68 -27
- package/templates/apps/web/src/lib/routes.ts +15 -1
- package/versions.json +48 -48
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
// Products list page. Reads ProductsList MDX via the content adapter and emits
|
|
2
2
|
// a JSON-LD CollectionPage via the dispatcher.
|
|
3
|
-
|
|
3
|
+
// It lists every Product page through one generated card section (#10121,
|
|
4
|
+
// apps/web/lib/hub-children.tsx — the /alternatives and /templates shape).
|
|
5
|
+
import type {
|
|
6
|
+
getMdxSlugs as defaultGetMdxSlugs,
|
|
7
|
+
getMdxSource as defaultGetMdxSource,
|
|
8
|
+
getPageSections as defaultGetPageSections,
|
|
9
|
+
} from "@working-theory/page-renderer";
|
|
4
10
|
import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
|
|
5
11
|
import type { Metadata } from "next";
|
|
6
12
|
import type { ReactElement } from "react";
|
|
7
13
|
|
|
14
|
+
import { type HubConfig, hubPageInput } from "../../../lib/hub-children";
|
|
15
|
+
|
|
8
16
|
interface Props {
|
|
9
17
|
params: Promise<{ locale: string }>;
|
|
10
18
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
19
|
+
getPageSections?: typeof defaultGetPageSections;
|
|
20
|
+
getMdxSlugs?: typeof defaultGetMdxSlugs;
|
|
11
21
|
}
|
|
12
22
|
|
|
23
|
+
const HUB: HubConfig = {
|
|
24
|
+
listType: "ProductsList",
|
|
25
|
+
slug: "products",
|
|
26
|
+
childType: "Product",
|
|
27
|
+
linkLabel: "View the product",
|
|
28
|
+
sectionId: "product-cards",
|
|
29
|
+
body: "mdx-body",
|
|
30
|
+
};
|
|
31
|
+
|
|
13
32
|
export async function generateMetadata({
|
|
14
33
|
params,
|
|
15
34
|
}: Pick<Props, "params">): Promise<Metadata> {
|
|
@@ -19,15 +38,12 @@ export async function generateMetadata({
|
|
|
19
38
|
|
|
20
39
|
export default async function Page({
|
|
21
40
|
params,
|
|
22
|
-
|
|
41
|
+
...loaders
|
|
23
42
|
}: Props): Promise<ReactElement> {
|
|
24
43
|
const { locale } = await params;
|
|
25
44
|
return renderPage({
|
|
26
45
|
type: "ProductsList",
|
|
27
46
|
slug: "products",
|
|
28
|
-
locale,
|
|
29
|
-
translationsNamespace: "products",
|
|
30
|
-
bodyMode: "mdx-body",
|
|
31
|
-
...(getMdxSource !== undefined && { getMdxSource }),
|
|
47
|
+
...(await hubPageInput(HUB, locale, loaders)),
|
|
32
48
|
});
|
|
33
49
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub child cards (#10121) for /projects. The shared cases live in
|
|
3
|
+
* `lib/__tests__/hub-children.fixtures.ts`; this file sits beside the route so
|
|
4
|
+
* a scaffold without /projects 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 ProjectsPage from "../page";
|
|
24
|
+
import {
|
|
25
|
+
describeHub,
|
|
26
|
+
type HubPage,
|
|
27
|
+
} from "../../../../lib/__tests__/hub-children.fixtures";
|
|
28
|
+
|
|
29
|
+
describeHub(
|
|
30
|
+
["projects", ProjectsPage as HubPage, "ProjectsList", "Project", "sections"],
|
|
31
|
+
mockRenderPage,
|
|
32
|
+
);
|
|
@@ -90,6 +90,8 @@ describe("ProjectsPage — snapshot regression", () => {
|
|
|
90
90
|
const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
|
|
91
91
|
const element = await ProjectsPage({
|
|
92
92
|
params: Promise.resolve({ locale: "en" }),
|
|
93
|
+
// No child pages: this snapshot pins the hub's own render (#10121).
|
|
94
|
+
getMdxSlugs: async () => [],
|
|
93
95
|
getMdxSource: getMdxSourceEmpty,
|
|
94
96
|
});
|
|
95
97
|
const { container } = render(element);
|
|
@@ -104,6 +106,8 @@ describe("ProjectsPage — snapshot regression", () => {
|
|
|
104
106
|
const getPageSectionsStub = jest.fn().mockResolvedValue([]);
|
|
105
107
|
const element = await ProjectsPage({
|
|
106
108
|
params: Promise.resolve({ locale: "en" }),
|
|
109
|
+
// No child pages: this snapshot pins the hub's own render (#10121).
|
|
110
|
+
getMdxSlugs: async () => [],
|
|
107
111
|
getMdxSource: getMdxSourcePopulated,
|
|
108
112
|
getPageSections: getPageSectionsStub,
|
|
109
113
|
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Projects listing page. Reads the ProjectsList MDX from the content backend,
|
|
2
2
|
// renders sections via SectionRenderer, and emits a JSON-LD <script>.
|
|
3
|
+
// It lists every Project page through one generated card section (#10121,
|
|
4
|
+
// apps/web/lib/hub-children.tsx — the /alternatives and /templates shape).
|
|
3
5
|
import type {
|
|
6
|
+
getMdxSlugs as defaultGetMdxSlugs,
|
|
4
7
|
getMdxSource as defaultGetMdxSource,
|
|
5
8
|
getPageSections as defaultGetPageSections,
|
|
6
9
|
} from "@working-theory/page-renderer";
|
|
@@ -8,35 +11,39 @@ import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
|
|
|
8
11
|
import type { Metadata } from "next";
|
|
9
12
|
import type { ReactElement } from "react";
|
|
10
13
|
|
|
14
|
+
import { type HubConfig, hubPageInput } from "../../../lib/hub-children";
|
|
15
|
+
|
|
11
16
|
interface Props {
|
|
12
17
|
params: Promise<{ locale: string }>;
|
|
13
18
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
14
19
|
getPageSections?: typeof defaultGetPageSections;
|
|
20
|
+
getMdxSlugs?: typeof defaultGetMdxSlugs;
|
|
15
21
|
}
|
|
16
22
|
|
|
23
|
+
const HUB: HubConfig = {
|
|
24
|
+
listType: "ProjectsList",
|
|
25
|
+
slug: "projects",
|
|
26
|
+
childType: "Project",
|
|
27
|
+
linkLabel: "View the project",
|
|
28
|
+
sectionId: "project-cards",
|
|
29
|
+
body: "sections",
|
|
30
|
+
};
|
|
31
|
+
|
|
17
32
|
export async function generateMetadata({
|
|
18
33
|
params,
|
|
19
34
|
}: Pick<Props, "params">): Promise<Metadata> {
|
|
20
35
|
const { locale } = await params;
|
|
21
|
-
return renderPageMetadata({
|
|
22
|
-
type: "ProjectsList",
|
|
23
|
-
slug: "projects",
|
|
24
|
-
locale,
|
|
25
|
-
});
|
|
36
|
+
return renderPageMetadata({ type: "ProjectsList", slug: "projects", locale });
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
export default async function Page({
|
|
29
40
|
params,
|
|
30
|
-
|
|
31
|
-
getPageSections,
|
|
41
|
+
...loaders
|
|
32
42
|
}: Props): Promise<ReactElement> {
|
|
33
43
|
const { locale } = await params;
|
|
34
44
|
return renderPage({
|
|
35
45
|
type: "ProjectsList",
|
|
36
46
|
slug: "projects",
|
|
37
|
-
locale,
|
|
38
|
-
translationsNamespace: "projects",
|
|
39
|
-
...(getMdxSource !== undefined && { getMdxSource }),
|
|
40
|
-
...(getPageSections !== undefined && { getPageSections }),
|
|
47
|
+
...(await hubPageInput(HUB, locale, loaders)),
|
|
41
48
|
});
|
|
42
49
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// Resource detail page. Reads the ResourceDetail MDX for the given slug, renders
|
|
2
|
-
// via renderPage
|
|
2
|
+
// via renderPage, and emits a JSON-LD <script>. Unknown slugs 404 through renderPage.
|
|
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
|
+
"ResourceDetail",
|
|
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: "ResourceDetail",
|
|
27
44
|
slug,
|
|
28
45
|
locale,
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
...(composed
|
|
47
|
+
? {}
|
|
48
|
+
: {
|
|
49
|
+
bodyMode: "mdx-body" as const,
|
|
50
|
+
populatedDescriptionTestId: "resource-description",
|
|
51
|
+
}),
|
|
31
52
|
...(getMdxSource !== undefined && { getMdxSource }),
|
|
32
53
|
});
|
|
33
54
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub child cards (#10121) for /resources. The shared cases live in
|
|
3
|
+
* `lib/__tests__/hub-children.fixtures.ts`; this file sits beside the route so
|
|
4
|
+
* a scaffold without /resources 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 ResourcesPage from "../page";
|
|
24
|
+
import {
|
|
25
|
+
describeHub,
|
|
26
|
+
type HubPage,
|
|
27
|
+
} from "../../../../lib/__tests__/hub-children.fixtures";
|
|
28
|
+
|
|
29
|
+
describeHub(
|
|
30
|
+
[
|
|
31
|
+
"resources",
|
|
32
|
+
ResourcesPage as HubPage,
|
|
33
|
+
"ResourcesHub",
|
|
34
|
+
"ResourceDetail",
|
|
35
|
+
"mdx-body",
|
|
36
|
+
],
|
|
37
|
+
mockRenderPage,
|
|
38
|
+
);
|
package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx
CHANGED
|
@@ -68,6 +68,8 @@ describe("ResourcesPage — snapshot regression", () => {
|
|
|
68
68
|
it("empty state renders deterministic HTML", async () => {
|
|
69
69
|
const element = await ResourcesPage({
|
|
70
70
|
params: Promise.resolve({ locale: "en" }),
|
|
71
|
+
// No child pages: this snapshot pins the hub's own render (#10121).
|
|
72
|
+
getMdxSlugs: async () => [],
|
|
71
73
|
getMdxSource: getMdxSourceEmpty,
|
|
72
74
|
});
|
|
73
75
|
const { container } = render(element);
|
|
@@ -90,11 +92,13 @@ describe("ResourcesPage — snapshot regression", () => {
|
|
|
90
92
|
|
|
91
93
|
const element = await ResourcesPage({
|
|
92
94
|
params: Promise.resolve({ locale: "en" }),
|
|
95
|
+
// No child pages: this snapshot pins the hub's own render (#10121).
|
|
96
|
+
getMdxSlugs: async () => [],
|
|
93
97
|
getMdxSource: getMdxSourcePopulated,
|
|
94
98
|
});
|
|
95
99
|
const { container } = render(element);
|
|
96
100
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
97
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Resources","description":"Guides, templates, and tools.","mainEntity":{"@type":"ItemList"}}</script><main id="main"><div><
|
|
101
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Resources","description":"Guides, templates, and tools.","mainEntity":{"@type":"ItemList"}}</script><main id="main"><div><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 data-testid="mdx-remote" data-source=""></div></article></div></div></div></main>"`,
|
|
98
102
|
);
|
|
99
103
|
});
|
|
100
104
|
});
|
|
@@ -124,7 +128,7 @@ describe("ResourceDetailPage — snapshot regression", () => {
|
|
|
124
128
|
});
|
|
125
129
|
const { container } = render(element);
|
|
126
130
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
127
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Getting Started","description":"A guide for new users.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"Getting Started","description":"A guide for new users."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Resources","item":"http://localhost:3000/en/resources"},{"@type":"ListItem","position":3,"name":"Getting Started","item":"http://localhost:3000/en/resources/getting-started"}]}]}</script><main id="main"><div><
|
|
131
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Getting Started","description":"A guide for new users.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"Getting Started","description":"A guide for new users."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Resources","item":"http://localhost:3000/en/resources"},{"@type":"ListItem","position":3,"name":"Getting Started","item":"http://localhost:3000/en/resources/getting-started"}]}]}</script><main id="main"><div><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 data-testid="mdx-remote" data-source=""></div></article></div></div></div></main>"`,
|
|
128
132
|
);
|
|
129
133
|
});
|
|
130
134
|
|
|
@@ -1,39 +1,49 @@
|
|
|
1
1
|
// Resources hub page. Reads ResourcesHub MDX via the content adapter and emits
|
|
2
2
|
// a JSON-LD CollectionPage via the dispatcher.
|
|
3
|
+
// It lists every ResourceDetail page through one generated card section (#10121,
|
|
4
|
+
// apps/web/lib/hub-children.tsx — the /alternatives and /templates shape).
|
|
3
5
|
import type {
|
|
6
|
+
getMdxSlugs as defaultGetMdxSlugs,
|
|
4
7
|
getMdxSource as defaultGetMdxSource,
|
|
8
|
+
getPageSections as defaultGetPageSections,
|
|
5
9
|
} from "@working-theory/page-renderer";
|
|
6
10
|
import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
|
|
7
11
|
import type { Metadata } from "next";
|
|
8
12
|
import type { ReactElement } from "react";
|
|
9
13
|
|
|
14
|
+
import { type HubConfig, hubPageInput } from "../../../lib/hub-children";
|
|
15
|
+
|
|
10
16
|
interface Props {
|
|
11
17
|
params: Promise<{ locale: string }>;
|
|
12
18
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
19
|
+
getPageSections?: typeof defaultGetPageSections;
|
|
20
|
+
getMdxSlugs?: typeof defaultGetMdxSlugs;
|
|
13
21
|
}
|
|
14
22
|
|
|
23
|
+
const HUB: HubConfig = {
|
|
24
|
+
listType: "ResourcesHub",
|
|
25
|
+
slug: "resources",
|
|
26
|
+
childType: "ResourceDetail",
|
|
27
|
+
linkLabel: "Read the resource",
|
|
28
|
+
sectionId: "resource-cards",
|
|
29
|
+
body: "mdx-body",
|
|
30
|
+
};
|
|
31
|
+
|
|
15
32
|
export async function generateMetadata({
|
|
16
33
|
params,
|
|
17
34
|
}: Pick<Props, "params">): Promise<Metadata> {
|
|
18
35
|
const { locale } = await params;
|
|
19
|
-
return renderPageMetadata({
|
|
20
|
-
type: "ResourcesHub",
|
|
21
|
-
slug: "resources",
|
|
22
|
-
locale,
|
|
23
|
-
});
|
|
36
|
+
return renderPageMetadata({ type: "ResourcesHub", slug: "resources", locale });
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
export default async function Page({
|
|
27
40
|
params,
|
|
28
|
-
|
|
41
|
+
...loaders
|
|
29
42
|
}: Props): Promise<ReactElement> {
|
|
30
43
|
const { locale } = await params;
|
|
31
44
|
return renderPage({
|
|
32
45
|
type: "ResourcesHub",
|
|
33
46
|
slug: "resources",
|
|
34
|
-
locale,
|
|
35
|
-
translationsNamespace: "resources",
|
|
36
|
-
bodyMode: "mdx-body",
|
|
37
|
-
...(getMdxSource !== undefined && { getMdxSource }),
|
|
47
|
+
...(await hubPageInput(HUB, locale, loaders)),
|
|
38
48
|
});
|
|
39
49
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// A service under its category (#10043 D2/D7): `/services/<category>/<slug>`,
|
|
2
|
+
// served at `<services base>/<category>/<slug>`. Gated with the `services`
|
|
3
|
+
// surface. The service's `parent` must name this category, and the category
|
|
4
|
+
// must be a Service with no parent of its own; anything else is a 404, so a
|
|
5
|
+
// service is served at exactly one URL.
|
|
6
|
+
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
7
|
+
import {
|
|
8
|
+
getMdxSource as loadMdxSource,
|
|
9
|
+
renderPage,
|
|
10
|
+
renderPageMetadata,
|
|
11
|
+
} from "@working-theory/page-renderer";
|
|
12
|
+
import type { Metadata } from "next";
|
|
13
|
+
import { notFound } from "next/navigation";
|
|
14
|
+
import type { ReactElement } from "react";
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
params: Promise<{ locale: string; slug: string; child: string }>;
|
|
18
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function generateMetadata({
|
|
22
|
+
params,
|
|
23
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
24
|
+
const { locale, child } = await params;
|
|
25
|
+
return renderPageMetadata({ type: "Service", slug: child, locale });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default async function Page({
|
|
29
|
+
params,
|
|
30
|
+
getMdxSource,
|
|
31
|
+
}: Props): Promise<ReactElement> {
|
|
32
|
+
const { locale, slug, child } = await params;
|
|
33
|
+
const load = getMdxSource ?? loadMdxSource;
|
|
34
|
+
const source = await load("Service", child, locale);
|
|
35
|
+
// A page that composes `sections[]` renders them (JSON-LD is built from
|
|
36
|
+
// the same sections); a prose page renders its MDX body — the rule
|
|
37
|
+
// /vs/[slug] applies (#10122).
|
|
38
|
+
const composed =
|
|
39
|
+
source !== null &&
|
|
40
|
+
Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
|
|
41
|
+
((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) >
|
|
42
|
+
0;
|
|
43
|
+
const render = () =>
|
|
44
|
+
renderPage({
|
|
45
|
+
type: "Service",
|
|
46
|
+
slug: child,
|
|
47
|
+
locale,
|
|
48
|
+
...(composed
|
|
49
|
+
? {}
|
|
50
|
+
: {
|
|
51
|
+
bodyMode: "mdx-body" as const,
|
|
52
|
+
populatedDescriptionTestId: "service-description",
|
|
53
|
+
}),
|
|
54
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
55
|
+
});
|
|
56
|
+
// A missing page is renderPage's to answer (#7537).
|
|
57
|
+
if (source === null) return render();
|
|
58
|
+
|
|
59
|
+
if (source.frontmatter.parent !== slug) notFound();
|
|
60
|
+
const category = await load("Service", slug, locale);
|
|
61
|
+
if (
|
|
62
|
+
category === null ||
|
|
63
|
+
category.frontmatter.parent !== undefined
|
|
64
|
+
) {
|
|
65
|
+
notFound();
|
|
66
|
+
}
|
|
67
|
+
return render();
|
|
68
|
+
}
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
// Service detail page. Reads the Service MDX for the given slug, renders via
|
|
2
|
-
//
|
|
2
|
+
// @working-theory/page-renderer, and emits a JSON-LD <script>. Unknown slugs 404 through renderPage.
|
|
3
|
+
//
|
|
4
|
+
// #10043 D7 — a service with a `parent` lives under its category, so its flat
|
|
5
|
+
// URL answers a permanent redirect there; a service that is a category lists
|
|
6
|
+
// its services after its body.
|
|
3
7
|
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
4
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getMdxSource as loadMdxSource,
|
|
10
|
+
renderPage,
|
|
11
|
+
renderPageMetadata,
|
|
12
|
+
} from "@working-theory/page-renderer";
|
|
13
|
+
import { SectionRenderer } from "@working-theory/ui";
|
|
5
14
|
import type { Metadata } from "next";
|
|
15
|
+
import { permanentRedirect } from "next/navigation";
|
|
16
|
+
import { getTranslations } from "next-intl/server";
|
|
6
17
|
import type { ReactElement } from "react";
|
|
7
18
|
|
|
19
|
+
import { listDetailPages } from "../../../../lib/route-family";
|
|
20
|
+
import {
|
|
21
|
+
childServices,
|
|
22
|
+
serviceChildrenSection,
|
|
23
|
+
servicePublicPath,
|
|
24
|
+
} from "../../../../lib/service-listing";
|
|
25
|
+
|
|
8
26
|
interface Props {
|
|
9
27
|
params: Promise<{ locale: string; slug: string }>;
|
|
10
28
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
@@ -22,12 +40,49 @@ export default async function Page({
|
|
|
22
40
|
getMdxSource,
|
|
23
41
|
}: Props): Promise<ReactElement> {
|
|
24
42
|
const { locale, slug } = await params;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
const load = getMdxSource ?? loadMdxSource;
|
|
44
|
+
const source = await load("Service", slug, locale);
|
|
45
|
+
// A page that composes `sections[]` renders them (JSON-LD is built from
|
|
46
|
+
// the same sections); a prose page renders its MDX body — the rule
|
|
47
|
+
// /vs/[slug] applies (#10122).
|
|
48
|
+
const composed =
|
|
49
|
+
source !== null &&
|
|
50
|
+
Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
|
|
51
|
+
((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) >
|
|
52
|
+
0;
|
|
53
|
+
// `trailing` is set only on a category (below), so every other render's
|
|
54
|
+
// input is the one this route has always passed.
|
|
55
|
+
const render = (trailing?: ReactElement) =>
|
|
56
|
+
renderPage({
|
|
57
|
+
type: "Service",
|
|
58
|
+
slug,
|
|
59
|
+
locale,
|
|
60
|
+
...(composed
|
|
61
|
+
? {}
|
|
62
|
+
: {
|
|
63
|
+
bodyMode: "mdx-body" as const,
|
|
64
|
+
populatedDescriptionTestId: "service-description",
|
|
65
|
+
}),
|
|
66
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
67
|
+
...(trailing !== undefined && { trailing }),
|
|
68
|
+
});
|
|
69
|
+
// A missing page is renderPage's to answer (#7537).
|
|
70
|
+
if (source === null) return render();
|
|
71
|
+
|
|
72
|
+
const parent = source.frontmatter.parent;
|
|
73
|
+
if (parent !== undefined) {
|
|
74
|
+
const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? "en";
|
|
75
|
+
const prefix = locale === defaultLocale ? "" : `/${locale}`;
|
|
76
|
+
permanentRedirect(`${prefix}${servicePublicPath(slug, parent)}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const children = childServices(await listDetailPages("Service", locale, load), slug);
|
|
80
|
+
if (children.length === 0) return render();
|
|
81
|
+
|
|
82
|
+
const t = await getTranslations({ namespace: "services", locale });
|
|
83
|
+
const list = serviceChildrenSection(children, {
|
|
84
|
+
childrenHeading: t("childrenHeading"),
|
|
85
|
+
viewService: t("viewService"),
|
|
32
86
|
});
|
|
87
|
+
return render(<SectionRenderer sections={[list]} locale={locale} />);
|
|
33
88
|
}
|