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
|
@@ -11,9 +11,14 @@ import {
|
|
|
11
11
|
renderPage,
|
|
12
12
|
renderPageMetadata
|
|
13
13
|
} from "@working-theory/page-renderer";
|
|
14
|
+
import type { HeaderStyle } from "@working-theory/site-config/schema";
|
|
15
|
+
import { localeHref } from "@working-theory/ui";
|
|
14
16
|
import type { Metadata } from "next";
|
|
17
|
+
import { redirect } from "next/navigation";
|
|
15
18
|
import type { ReactElement } from "react";
|
|
16
19
|
|
|
20
|
+
import { MARKETING_PAGES } from "../../lib/modules.generated";
|
|
21
|
+
import { resolveHomeChrome } from "../../lib/page-chrome";
|
|
17
22
|
import { siteChrome, type SiteChrome } from "../../lib/site-chrome";
|
|
18
23
|
import { siteLogomarkAsset, siteWordmarkAsset } from "../../lib/site-logo";
|
|
19
24
|
|
|
@@ -39,8 +44,16 @@ interface Props {
|
|
|
39
44
|
*/
|
|
40
45
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
41
46
|
getPageSections?: typeof defaultGetPageSections;
|
|
42
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* The home's layout; defaults to the resolved one (#10049 D6) — the home's
|
|
49
|
+
* own `layout:`, else `_site.mdx`'s, else the generated `siteChrome`'s.
|
|
50
|
+
*/
|
|
43
51
|
home?: SiteChrome["home"];
|
|
52
|
+
/**
|
|
53
|
+
* The home's header style (#10049 D6); defaults to the resolved one. When
|
|
54
|
+
* `home` is passed and this is not, it is `bare` for a deck, else `full`.
|
|
55
|
+
*/
|
|
56
|
+
homeHeader?: HeaderStyle;
|
|
44
57
|
/** The design's `layout.deckGrounds`; defaults to the generated `siteChrome`'s. */
|
|
45
58
|
deckGrounds?: DeckGrounds;
|
|
46
59
|
/** The design's `layout.deckPresentation` (#7740); defaults to the generated `siteChrome`'s. */
|
|
@@ -53,19 +66,44 @@ export async function generateMetadata({
|
|
|
53
66
|
params,
|
|
54
67
|
}: Pick<Props, "params">): Promise<Metadata> {
|
|
55
68
|
const { locale } = await params;
|
|
69
|
+
// #10040 decision 1 — a site without the marketing module has no home page.
|
|
70
|
+
if (!MARKETING_PAGES) return {};
|
|
56
71
|
return renderPageMetadata({ type: "HomePage", slug: "home", locale });
|
|
57
72
|
}
|
|
58
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The home's layout and header (#10049 D6). A caller that passes `home`
|
|
76
|
+
* keeps the pre-#10049 behaviour: a deck's header is bare.
|
|
77
|
+
*/
|
|
78
|
+
async function resolveHome(
|
|
79
|
+
locale: string,
|
|
80
|
+
home: SiteChrome["home"] | undefined,
|
|
81
|
+
homeHeader: HeaderStyle | undefined,
|
|
82
|
+
): Promise<{ home: SiteChrome["home"]; homeHeader: HeaderStyle }> {
|
|
83
|
+
if (home !== undefined) {
|
|
84
|
+
return { home, homeHeader: homeHeader ?? (home === "deck" ? "bare" : "full") };
|
|
85
|
+
}
|
|
86
|
+
const resolved = await resolveHomeChrome(locale, chrome);
|
|
87
|
+
return { home: resolved.layout, homeHeader: homeHeader ?? resolved.chrome.header };
|
|
88
|
+
}
|
|
89
|
+
|
|
59
90
|
export default async function Page({
|
|
60
91
|
params,
|
|
61
92
|
getMdxSource,
|
|
62
93
|
getPageSections,
|
|
63
|
-
home
|
|
94
|
+
home: homeProp,
|
|
95
|
+
homeHeader: homeHeaderProp,
|
|
64
96
|
deckGrounds = chrome.deckGrounds,
|
|
65
97
|
deckPresentation = chrome.deckPresentation,
|
|
66
98
|
deckAnchors = chrome.deckAnchors,
|
|
67
99
|
}: Props): Promise<ReactElement> {
|
|
68
100
|
const { locale } = await params;
|
|
101
|
+
// #10040 decision 1 — on a site without the marketing module (a hub) `/`
|
|
102
|
+
// goes to the account area; signed out, the authed gate sends it on to
|
|
103
|
+
// sign-in. Read before any content, so a hub never renders a missing
|
|
104
|
+
// HomePage seed.
|
|
105
|
+
if (!MARKETING_PAGES) redirect(localeHref(locale, "/account"));
|
|
106
|
+
const { home, homeHeader } = await resolveHome(locale, homeProp, homeHeaderProp);
|
|
69
107
|
return renderPage({
|
|
70
108
|
type: "HomePage",
|
|
71
109
|
slug: "home",
|
|
@@ -80,12 +118,22 @@ export default async function Page({
|
|
|
80
118
|
// landing with the site's wordmark in the headline's place; any other
|
|
81
119
|
// leading hero keeps its composition and takes the logomark (#7711).
|
|
82
120
|
// Content that authors its own lockup keeps it.
|
|
83
|
-
|
|
121
|
+
// #10049 D5 — under a full header the home keeps its bar, so no landing.
|
|
122
|
+
...(home === "deck" &&
|
|
123
|
+
homeHeader !== "full" && {
|
|
84
124
|
deckLanding: { wordmark: siteWordmarkAsset(), logomark: siteLogomarkAsset() },
|
|
85
125
|
}),
|
|
86
126
|
// #7719 — a deck home cycles its design's ground sequence, slide by
|
|
87
127
|
// slide, onto every slide that authors no surface of its own.
|
|
88
128
|
...(home === "deck" && deckGrounds !== undefined && { deckGrounds }),
|
|
129
|
+
// #10049 — a deck home under a full header passes no landing, so on a
|
|
130
|
+
// design with no grounds it would carry none of the deck inputs and
|
|
131
|
+
// renderPage would treat it as a plain page, emitting a section-anchor
|
|
132
|
+
// span before each section — each one a full-height snap slide. An empty
|
|
133
|
+
// ground sequence (a documented no-op, `applyDeckGrounds`) keeps it a deck.
|
|
134
|
+
...(home === "deck" &&
|
|
135
|
+
homeHeader === "full" &&
|
|
136
|
+
deckGrounds === undefined && { deckGrounds: [] }),
|
|
89
137
|
// #7740 — a deck home under the presentation dial: every slide with an
|
|
90
138
|
// alignment field centred, card grids one column on phones, and slide
|
|
91
139
|
// anchors clear of the page's JSON-LD @id fragments.
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// Partner detail page. Reads the Partner 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
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,11 +26,20 @@ 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)("Partner", slug, locale);
|
|
33
|
+
const composed =
|
|
34
|
+
source !== null &&
|
|
35
|
+
Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
|
|
36
|
+
((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) >
|
|
37
|
+
0;
|
|
25
38
|
return renderPage({
|
|
26
39
|
type: "Partner",
|
|
27
40
|
slug,
|
|
28
41
|
locale,
|
|
29
|
-
bodyMode: "mdx-body",
|
|
42
|
+
...(composed ? {} : { bodyMode: "mdx-body" as const }),
|
|
30
43
|
...(getMdxSource !== undefined && { getMdxSource }),
|
|
31
44
|
});
|
|
32
45
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub child cards (#10121) for /partners. The shared cases live in
|
|
3
|
+
* `lib/__tests__/hub-children.fixtures.ts`; this file sits beside the route so
|
|
4
|
+
* a scaffold without /partners 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 PartnersPage from "../page";
|
|
24
|
+
import {
|
|
25
|
+
describeHub,
|
|
26
|
+
type HubPage,
|
|
27
|
+
} from "../../../../lib/__tests__/hub-children.fixtures";
|
|
28
|
+
|
|
29
|
+
describeHub(
|
|
30
|
+
["partners", PartnersPage as HubPage, "PartnersList", "Partner", "mdx-body"],
|
|
31
|
+
mockRenderPage,
|
|
32
|
+
);
|
|
@@ -43,6 +43,8 @@ describe("partners/page — snapshot regression", () => {
|
|
|
43
43
|
it("empty state renders deterministic HTML", async () => {
|
|
44
44
|
const element = await PartnersPage({
|
|
45
45
|
params: Promise.resolve({ locale: "en" }),
|
|
46
|
+
// No child pages: this snapshot pins the hub's own render (#10121).
|
|
47
|
+
getMdxSlugs: async () => [],
|
|
46
48
|
getMdxSource: getMdxSourceEmpty,
|
|
47
49
|
});
|
|
48
50
|
const { container } = render(element);
|
|
@@ -64,11 +66,13 @@ describe("partners/page — snapshot regression", () => {
|
|
|
64
66
|
|
|
65
67
|
const element = await PartnersPage({
|
|
66
68
|
params: Promise.resolve({ locale: "en" }),
|
|
69
|
+
// No child pages: this snapshot pins the hub's own render (#10121).
|
|
70
|
+
getMdxSlugs: async () => [],
|
|
67
71
|
getMdxSource: getMdxSourcePopulated,
|
|
68
72
|
});
|
|
69
73
|
const { container } = render(element);
|
|
70
74
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
71
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Partners","description":"Organisations partnering with AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Organization"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><
|
|
75
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Partners","description":"Organisations partnering with AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Organization"}]}}</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">mdx-body-content</div></div></article></div></div></div></main>"`,
|
|
72
76
|
);
|
|
73
77
|
});
|
|
74
78
|
});
|
|
@@ -93,7 +97,7 @@ describe("partners/[slug]/page — snapshot regression", () => {
|
|
|
93
97
|
});
|
|
94
98
|
const { container } = render(element);
|
|
95
99
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
96
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Acme Corp","description":"Acme Corp is a strategic partner.","mainEntity":{"@context":"https://schema.org","@type":"Organization","name":"Acme Corp","description":"Acme Corp is a strategic partner."}},{"@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":"Partners","item":"http://localhost:3000/en/partners"},{"@type":"ListItem","position":3,"name":"Acme Corp","item":"http://localhost:3000/en/partners/acme-corp"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><
|
|
100
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Acme Corp","description":"Acme Corp is a strategic partner.","mainEntity":{"@context":"https://schema.org","@type":"Organization","name":"Acme Corp","description":"Acme Corp is a strategic partner."}},{"@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":"Partners","item":"http://localhost:3000/en/partners"},{"@type":"ListItem","position":3,"name":"Acme Corp","item":"http://localhost:3000/en/partners/acme-corp"}]}]}</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">mdx-body-content</div></div></article></div></div></div></main>"`,
|
|
97
101
|
);
|
|
98
102
|
});
|
|
99
103
|
|
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
// Partners list page. Reads PartnersList MDX via the content adapter and emits
|
|
2
2
|
// a JSON-LD CollectionPage via the dispatcher.
|
|
3
|
-
|
|
3
|
+
// It lists every Partner 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: "PartnersList",
|
|
25
|
+
slug: "partners",
|
|
26
|
+
childType: "Partner",
|
|
27
|
+
linkLabel: "Meet the partner",
|
|
28
|
+
sectionId: "partner-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: "PartnersList",
|
|
27
46
|
slug: "partners",
|
|
28
|
-
locale,
|
|
29
|
-
translationsNamespace: "partners",
|
|
30
|
-
bodyMode: "mdx-body",
|
|
31
|
-
...(getMdxSource !== undefined && { getMdxSource }),
|
|
47
|
+
...(await hubPageInput(HUB, locale, loaders)),
|
|
32
48
|
});
|
|
33
49
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub child cards (#10121) for /portfolio. The shared cases live in
|
|
3
|
+
* `lib/__tests__/hub-children.fixtures.ts`; this file sits beside the route so
|
|
4
|
+
* a scaffold without /portfolio 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 PortfolioPage from "../page";
|
|
24
|
+
import {
|
|
25
|
+
describeHub,
|
|
26
|
+
type HubPage,
|
|
27
|
+
} from "../../../../lib/__tests__/hub-children.fixtures";
|
|
28
|
+
|
|
29
|
+
describeHub(
|
|
30
|
+
[
|
|
31
|
+
"portfolio",
|
|
32
|
+
PortfolioPage as HubPage,
|
|
33
|
+
"PortfolioList",
|
|
34
|
+
"PortfolioItem",
|
|
35
|
+
"composed",
|
|
36
|
+
],
|
|
37
|
+
mockRenderPage,
|
|
38
|
+
);
|
|
@@ -1,49 +1,50 @@
|
|
|
1
1
|
// Portfolio list page (#6687). Reads PortfolioList 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 PortfolioItem 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: "PortfolioList",
|
|
26
|
+
slug: "portfolio",
|
|
27
|
+
childType: "PortfolioItem",
|
|
28
|
+
linkLabel: "See the project",
|
|
29
|
+
sectionId: "portfolio-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: "PortfolioList",
|
|
26
|
-
slug: "portfolio",
|
|
27
|
-
locale,
|
|
28
|
-
});
|
|
37
|
+
return renderPageMetadata({ type: "PortfolioList", slug: "portfolio", 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)("PortfolioList", "portfolio", 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: "PortfolioList",
|
|
43
47
|
slug: "portfolio",
|
|
44
|
-
locale,
|
|
45
|
-
translationsNamespace: "portfolio",
|
|
46
|
-
...(composed ? {} : { bodyMode: "mdx-body" as const }),
|
|
47
|
-
...(getMdxSource !== undefined && { getMdxSource }),
|
|
48
|
+
...(await hubPageInput(HUB, locale, loaders)),
|
|
48
49
|
});
|
|
49
50
|
}
|