create-ailk 0.7.0 → 0.8.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.
Files changed (169) hide show
  1. package/README.md +44 -10
  2. package/component-catalog.json +520 -5
  3. package/component-catalog.md +9 -6
  4. package/dist/cli.js +22 -24
  5. package/dist/lib/extract-module-bundle.js +5 -1
  6. package/dist/lib/module-license-gate.d.ts +79 -0
  7. package/dist/lib/module-license-gate.js +160 -12
  8. package/dist/lib/module-tier.d.ts +45 -16
  9. package/dist/lib/module-tier.js +76 -0
  10. package/dist/lib/pro-callout.js +10 -2
  11. package/dist/parse-args.d.ts +78 -10
  12. package/dist/parse-args.js +204 -14
  13. package/dist/programmatic.d.ts +39 -9
  14. package/dist/programmatic.js +163 -25
  15. package/dist/site-roster.js +11 -6
  16. package/dist/starter-grid.d.ts +216 -0
  17. package/dist/starter-grid.js +449 -0
  18. package/dist/surfaces.d.ts +4 -4
  19. package/dist/surfaces.js +102 -43
  20. package/dist/template-manifest.d.ts +36 -17
  21. package/dist/template-manifest.js +121 -367
  22. package/dist/template-names.d.ts +14 -12
  23. package/dist/template-names.js +13 -17
  24. package/package.json +3 -3
  25. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -1
  26. package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
  27. package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
  28. package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
  29. package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
  30. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
  31. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
  32. package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
  33. package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
  34. package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
  35. package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
  36. package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
  37. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
  38. package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
  39. package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
  40. package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
  41. package/site-templates/designs/standard/DESIGN.md +5 -0
  42. package/site-templates/marketing-site/DESIGN.md +5 -0
  43. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +89 -25
  44. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +35 -14
  45. package/site-templates/marketing-site-free/DESIGN.md +5 -0
  46. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +95 -29
  47. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +74 -44
  48. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
  49. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
  50. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +17 -29
  51. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -1
  52. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  53. package/site-templates/saas-product/DESIGN.md +5 -0
  54. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
  55. package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +96 -76
  56. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
  57. package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
  58. package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
  59. package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
  60. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  61. package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
  62. package/site-templates/{simple-service → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
  63. package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
  64. package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
  65. package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
  66. package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
  67. package/site-templates/shapes/local-business-service/provenance.json +4 -0
  68. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
  69. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
  70. package/site-templates/shapes/organization-service/ailk-template.json +10 -0
  71. package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
  72. package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
  73. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  74. package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
  75. package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  76. package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
  77. package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
  78. package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
  79. package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
  80. package/site-templates/shapes/organization-service/provenance.json +4 -0
  81. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
  82. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
  83. package/site-templates/shapes/person-service/ailk-template.json +10 -0
  84. package/site-templates/shapes/person-service/content/_site.mdx +85 -0
  85. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
  86. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
  87. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  88. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
  89. package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  90. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
  91. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
  92. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
  93. package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
  94. package/site-templates/shapes/person-service/provenance.json +4 -0
  95. package/templates/.claude/skills/README.md +3 -2
  96. package/templates/.claude/skills/add-page/references/page-types.md +4 -0
  97. package/templates/.claude/skills/setup-database/SKILL.md +156 -0
  98. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
  99. package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
  100. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +19 -28
  101. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
  102. package/templates/DESIGN.md +5 -0
  103. package/templates/README.md +46 -38
  104. package/templates/apps/api/CLAUDE.md +3 -2
  105. package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
  106. package/templates/apps/api/src/config/index.ts +35 -16
  107. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
  108. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
  109. package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
  110. package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
  111. package/templates/apps/api/src/openapi/spec.ts +63 -12
  112. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
  113. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
  114. package/templates/apps/api/src/routes/leads/index.ts +46 -1
  115. package/templates/apps/api/src/server.ts +7 -0
  116. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
  117. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
  118. package/templates/apps/api/src/services/lead-routing.ts +140 -30
  119. package/templates/apps/api/src/services/leads-inbox.ts +48 -11
  120. package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
  121. package/templates/apps/mcp/src/tools/create_project.ts +21 -6
  122. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
  123. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
  124. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
  125. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
  126. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
  127. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
  128. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
  129. package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
  130. package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
  131. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
  132. package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
  133. package/templates/apps/web/app/[locale]/layout.tsx +37 -5
  134. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
  135. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
  136. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
  137. package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
  138. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
  139. package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
  140. package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +18 -3
  141. package/templates/apps/web/lib/nav-config.ts +48 -36
  142. package/templates/apps/web/lib/site-identity.ts +14 -0
  143. package/templates/apps/web/messages/en.json +28 -2
  144. package/templates/content/_site.mdx +4 -2
  145. package/templates/content/en/BlogList/blog.mdx +37 -16
  146. package/templates/content/en/ContactPage/contact.mdx +14 -44
  147. package/templates/content/en/HomePage/home.mdx +8 -16
  148. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  149. package/templates/content/en/UseCasesList/use-cases.mdx +6 -6
  150. package/templates/database/CHANGELOG.md +7 -0
  151. package/templates/database/package.json +1 -1
  152. package/versions.json +33 -33
  153. package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
  154. package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
  155. package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
  156. package/site-templates/simple-service/DESIGN.md +0 -162
  157. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
  158. package/site-templates/simple-service/ailk-template.json +0 -11
  159. package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
  160. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
  161. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
  162. package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
  163. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
  164. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
  165. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
  166. package/site-templates/simple-service/provenance.json +0 -4
  167. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
  168. package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
  169. package/templates/content/en/SoftwareProduct/product.mdx +0 -175
@@ -0,0 +1,66 @@
1
+ "use server";
2
+
3
+ /**
4
+ * subscribeNewsletterAction — Server Action paired with the
5
+ * `ailkSubscribeNewsletter` MCP tool (#6730 D9).
6
+ *
7
+ * `/newsletter` renders the shared `LeadCaptureForm` bound to this action
8
+ * when the API accepts a subscription (a database is set up) and the page's
9
+ * content declares no signup section of its own. Validates the address with
10
+ * the tool's own input schema, calls `POST /v1/leads/subscribe` through
11
+ * `@working-theory/api-client`, and maps the outcome the same way
12
+ * `captureLeadAction` does: the API's 503 `lead_capture_not_configured` →
13
+ * `unavailable`; any other non-2xx or thrown error → the generic `error`;
14
+ * only a 2xx is `success`.
15
+ */
16
+
17
+ import { leads } from "@working-theory/api-client";
18
+ import { LEAD_CAPTURE_HONEYPOT, type LeadCaptureFormState } from "@working-theory/ui";
19
+ import { subscribeNewsletterInputSchema } from "@working-theory/validation";
20
+
21
+ const GENERIC_ERROR = "Something went wrong, please try again.";
22
+
23
+ export async function subscribeNewsletterAction(
24
+ _prevState: LeadCaptureFormState,
25
+ formData: FormData,
26
+ ): Promise<LeadCaptureFormState> {
27
+ // Honeypot: a bot that fills the hidden field gets a person's answer, and
28
+ // nothing is sent.
29
+ const honeypot = formData.get(LEAD_CAPTURE_HONEYPOT);
30
+ if (typeof honeypot === "string" && honeypot !== "") return { kind: "success" };
31
+
32
+ const parsed = subscribeNewsletterInputSchema.safeParse({
33
+ email: formData.get("email"),
34
+ });
35
+ if (!parsed.success) {
36
+ return {
37
+ kind: "error",
38
+ fieldErrors: { email: parsed.error.issues[0]?.message ?? "Invalid email" },
39
+ };
40
+ }
41
+
42
+ const siteId = process.env.AILK_SITE_ID ?? "default";
43
+ try {
44
+ const res = await leads.subscribe({
45
+ siteId,
46
+ source: "newsletter",
47
+ email: parsed.data.email,
48
+ });
49
+ if (!res.ok) {
50
+ const error =
51
+ typeof res.body === "object" && res.body !== null
52
+ ? (res.body as { error?: unknown }).error
53
+ : undefined;
54
+ if (res.status === 503 && error === "lead_capture_not_configured") {
55
+ return { kind: "unavailable" };
56
+ }
57
+ return { kind: "error", message: GENERIC_ERROR };
58
+ }
59
+ return { kind: "success" };
60
+ } catch (err) {
61
+ // The api-client never attaches the request payload to a thrown error,
62
+ // so logging `err` carries no subscriber address.
63
+ console.error("[subscribeNewsletterAction]", err);
64
+ return { kind: "error", message: GENERIC_ERROR };
65
+ }
66
+ }
@@ -1,21 +1,78 @@
1
- // Newsletter page (#6658 D5) — a NewsletterSignup at /newsletter. Its
2
- // `newsletter` section posts to lead capture (`/v1/leads/subscribe`), which
3
- // persists a Subscriber and sends no verification email. A module-less
4
- // surface a template enables by naming it in its roster; with no content the
5
- // route 404s. Mirrors use-cases/page.tsx: a sections-composed page renders its
6
- // sections, a prose-only page its MDX body.
7
- import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
1
+ // Newsletter page (#6658 D5) — a NewsletterSignup at /newsletter. A
2
+ // module-less surface a template enables by naming it in its roster; with no
3
+ // content the route 404s. Mirrors use-cases/page.tsx: a sections-composed
4
+ // page renders its sections, a prose-only page its MDX body.
5
+ //
6
+ // A signup only when there is somewhere to keep it (#6730 D9). The API says
7
+ // whether it accepts a subscription (`GET /v1/leads/capture` — a database is
8
+ // set up):
9
+ //
10
+ // - accepting: a content `newsletter` section renders as authored (its own
11
+ // form posts to `/v1/leads/subscribe`); a page with no such section gets
12
+ // the shared `LeadCaptureForm` (one email field) appended, bound to
13
+ // `subscribeNewsletterAction`.
14
+ // - otherwise — including an unreachable API — every `newsletter` section
15
+ // is dropped before render, and a leading hero's CTA that pointed at a
16
+ // dropped section's anchor goes with it, so no signup form of any kind
17
+ // renders and no button points at a section that is gone. The API
18
+ // refuses a subscribe in that state anyway, so a cached page can never
19
+ // record a false success.
20
+ import { leads } from "@working-theory/api-client";
21
+ import type {
22
+ getMdxSource as defaultGetMdxSource,
23
+ getPageSections as defaultGetPageSections,
24
+ } from "@working-theory/page-renderer";
8
25
  import {
9
26
  getMdxSource as loadMdxSource,
27
+ getPageSections as loadPageSections,
10
28
  renderPage,
11
29
  renderPageMetadata,
12
30
  } from "@working-theory/page-renderer";
31
+ import { LeadCaptureForm, localeHref } from "@working-theory/ui";
13
32
  import type { Metadata } from "next";
33
+ import { getTranslations } from "next-intl/server";
14
34
  import type { ReactElement } from "react";
15
35
 
36
+ import { subscribeNewsletterAction } from "./newsletter.actions";
37
+
38
+ export const revalidate = 60;
39
+
16
40
  interface Props {
17
41
  params: Promise<{ locale: string }>;
18
42
  getMdxSource?: typeof defaultGetMdxSource;
43
+ getPageSections?: typeof defaultGetPageSections;
44
+ }
45
+
46
+ type Section = Awaited<ReturnType<typeof defaultGetPageSections>>[number];
47
+
48
+ /** Whether the API accepts a subscription; `false` for any failed read. */
49
+ async function acceptsSubscription(): Promise<boolean> {
50
+ try {
51
+ const res = await leads.captureStatus();
52
+ return res.ok && res.body.ok === true && res.body.data.newsletter;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Drop every `newsletter` section, and any leading-hero CTA whose `href` is
60
+ * the `#<id>` anchor of a section that was dropped.
61
+ */
62
+ function withoutSignup(sections: Section[]): Section[] {
63
+ const dropped = new Set(
64
+ sections.filter((s) => s.type === "newsletter").map((s) => `#${s.id}`),
65
+ );
66
+ if (dropped.size === 0) return sections;
67
+ return sections
68
+ .filter((s) => s.type !== "newsletter")
69
+ .map((s, i) => {
70
+ if (i !== 0 || s.type !== "hero") return s;
71
+ const data = { ...s.data };
72
+ if (data.primaryCta && dropped.has(data.primaryCta.href)) delete data.primaryCta;
73
+ if (data.secondaryCta && dropped.has(data.secondaryCta.href)) delete data.secondaryCta;
74
+ return { ...s, data };
75
+ });
19
76
  }
20
77
 
21
78
  export async function generateMetadata({
@@ -32,19 +89,54 @@ export async function generateMetadata({
32
89
  export default async function Page({
33
90
  params,
34
91
  getMdxSource,
92
+ getPageSections,
35
93
  }: Props): Promise<ReactElement> {
36
94
  const { locale } = await params;
37
- const source = await (getMdxSource ?? loadMdxSource)("NewsletterSignup", "newsletter", locale);
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
- return renderPage({
95
+ const load = getMdxSource ?? loadMdxSource;
96
+ const loadSections = getPageSections ?? loadPageSections;
97
+ const [source, accepting] = await Promise.all([
98
+ load("NewsletterSignup", "newsletter", locale),
99
+ acceptsSubscription(),
100
+ ]);
101
+ const declared = source ? await loadSections(source.frontmatter) : [];
102
+ const composed = declared.length > 0;
103
+ const hasSignupSection = declared.some((s) => s.type === "newsletter");
104
+
105
+ const rendered = await renderPage({
44
106
  type: "NewsletterSignup",
45
107
  slug: "newsletter",
46
108
  locale,
47
109
  ...(composed ? {} : { bodyMode: "mdx-body" as const }),
48
110
  ...(getMdxSource !== undefined && { getMdxSource }),
111
+ getPageSections: accepting
112
+ ? loadSections
113
+ : async (frontmatter: unknown) => withoutSignup(await loadSections(frontmatter)),
49
114
  });
115
+
116
+ if (!accepting || hasSignupSection || source === null) return rendered;
117
+
118
+ const [t, tc] = await Promise.all([
119
+ getTranslations({ locale, namespace: "newsletter" }),
120
+ getTranslations({ locale, namespace: "contact" }),
121
+ ]);
122
+ return (
123
+ <>
124
+ {rendered}
125
+ <LeadCaptureForm
126
+ action={subscribeNewsletterAction}
127
+ fields={[{ name: "email", label: t("emailLabel"), type: "email", required: true }]}
128
+ termsHref={localeHref(locale, "/terms")}
129
+ text={{
130
+ submit: t("submit"),
131
+ sending: t("sending"),
132
+ success: t("success"),
133
+ consentStatement: t("consentStatement"),
134
+ genericError: tc("genericError"),
135
+ unavailableHeading: tc("unavailableHeading"),
136
+ unavailableBody: tc("unavailableBody", { email: "{email}" }),
137
+ unavailableNoEmail: tc("unavailableNoEmail"),
138
+ }}
139
+ />
140
+ </>
141
+ );
50
142
  }
@@ -0,0 +1,59 @@
1
+ // Portfolio item page (#6687). Reads the PortfolioItem MDX for the given slug and emits a
2
+ // JSON-LD <script>. Unknown slugs trigger 404. Renders `sections[]` when the
3
+ // frontmatter carries them (the use-cases composed pattern), MDX prose otherwise.
4
+ // NO_HAND_IMPLEMENT_OK: /build-starter operator-directed live pass (#6687).
5
+ import {
6
+ getMdxSource as defaultGetMdxSource,
7
+ getMdxSlugs,
8
+ renderPage,
9
+ renderPageMetadata,
10
+ } from "@working-theory/page-renderer";
11
+ import type { Metadata } from "next";
12
+ import { notFound } from "next/navigation";
13
+ import type { ReactElement } from "react";
14
+
15
+ interface Props {
16
+ params: Promise<{ locale: string; slug: string }>;
17
+ getMdxSource?: typeof defaultGetMdxSource;
18
+ }
19
+
20
+ export async function generateMetadata({
21
+ params,
22
+ }: Pick<Props, "params">): Promise<Metadata> {
23
+ const { locale, slug } = await params;
24
+ return renderPageMetadata({ type: "PortfolioItem", slug, locale });
25
+ }
26
+
27
+ export async function generateStaticParams({
28
+ params,
29
+ }: {
30
+ params: { locale: string };
31
+ }): Promise<{ slug: string }[]> {
32
+ const slugs = await getMdxSlugs("PortfolioItem", params.locale);
33
+ return slugs.map((slug: string) => ({ slug }));
34
+ }
35
+
36
+ export default async function Page({
37
+ params,
38
+ getMdxSource = defaultGetMdxSource,
39
+ }: Props): Promise<ReactElement> {
40
+ const { locale, slug } = await params;
41
+ const source = await getMdxSource("PortfolioItem", slug, locale);
42
+
43
+ if (!source) {
44
+ notFound();
45
+ }
46
+
47
+ const composed =
48
+ Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
49
+ ((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) > 0;
50
+
51
+ return renderPage({
52
+ type: "PortfolioItem",
53
+ slug,
54
+ locale,
55
+ translationsNamespace: "portfolio",
56
+ ...(composed ? {} : { bodyMode: "mdx-body" as const }),
57
+ getMdxSource: async () => source,
58
+ });
59
+ }
@@ -0,0 +1,49 @@
1
+ // Portfolio list page (#6687). Reads PortfolioList MDX via the content adapter and emits
2
+ // a JSON-LD CollectionPage via the dispatcher.
3
+ // NO_HAND_IMPLEMENT_OK: /build-starter operator-directed live pass (#6687) — the
4
+ // use-cases composed pattern mirrored onto the portfolio hub: sections when the
5
+ // frontmatter carries `sections[]`, MDX prose otherwise (existing prose pages unchanged).
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 type { ReactElement } from "react";
14
+
15
+ interface Props {
16
+ params: Promise<{ locale: string }>;
17
+ getMdxSource?: typeof defaultGetMdxSource;
18
+ }
19
+
20
+ export async function generateMetadata({
21
+ params,
22
+ }: Pick<Props, "params">): Promise<Metadata> {
23
+ const { locale } = await params;
24
+ return renderPageMetadata({
25
+ type: "PortfolioList",
26
+ slug: "portfolio",
27
+ locale,
28
+ });
29
+ }
30
+
31
+ export default async function Page({
32
+ params,
33
+ getMdxSource,
34
+ }: Props): Promise<ReactElement> {
35
+ 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
+ return renderPage({
42
+ type: "PortfolioList",
43
+ slug: "portfolio",
44
+ locale,
45
+ translationsNamespace: "portfolio",
46
+ ...(composed ? {} : { bodyMode: "mdx-body" as const }),
47
+ ...(getMdxSource !== undefined && { getMdxSource }),
48
+ });
49
+ }
@@ -1,9 +1,11 @@
1
1
  // UseCase detail page (#6658 D4) — one use case at /use-cases/<slug>, listed by
2
2
  // the /use-cases page. Reads the UseCase MDX for the given slug, renders via
3
3
  // @working-theory/page-renderer, and emits a JSON-LD WebPage whose mainEntity
4
- // is an Article. Unknown slugs 404. Same shape as software/[slug]/page.tsx.
4
+ // is an Article. Unknown slugs 404. Composed `sections[]` render as sections
5
+ // (#6721); a prose page renders its MDX body.
5
6
  import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
6
7
  import {
8
+ getMdxSource as loadMdxSource,
7
9
  getMdxSlugs,
8
10
  renderPage,
9
11
  renderPageMetadata,
@@ -37,12 +39,25 @@ export default async function Page({
37
39
  getMdxSource,
38
40
  }: Props): Promise<ReactElement> {
39
41
  const { locale, slug } = await params;
42
+ // A page that composes `sections[]` (the starter seeds, #6721) renders
43
+ // them; a prose page renders its MDX body — the same rule the /use-cases hub
44
+ // applies. JSON-LD is built from the frontmatter either way.
45
+ const source = await (getMdxSource ?? loadMdxSource)("UseCase", slug, locale);
46
+ const composed =
47
+ source !== null &&
48
+ Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
49
+ ((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) >
50
+ 0;
40
51
  return renderPage({
41
52
  type: "UseCase",
42
53
  slug,
43
54
  locale,
44
- bodyMode: "mdx-body",
45
- populatedDescriptionTestId: "use-case-description",
55
+ ...(composed
56
+ ? {}
57
+ : {
58
+ bodyMode: "mdx-body" as const,
59
+ populatedDescriptionTestId: "use-case-description",
60
+ }),
46
61
  ...(getMdxSource !== undefined && { getMdxSource }),
47
62
  });
48
63
  }
@@ -1,30 +1,14 @@
1
- // nav-config.ts — Nav item registry for apps/web (#1451, re-shaped by #5469,
2
- // #6508).
1
+ // nav-config.ts — site-nav registry.
3
2
  //
4
- // The Free marketing-site template's nav (the approved chrome, #5469 D5,
5
- // #6508 D5): a Product flyout (How it works · Features · FAQ) rendered as a
6
- // fixed-640px two-column panel (the Standard Issue direction — #6508), then
7
- // How it works, Pricing and About as top-level items. Paths are
8
- // locale-agnostic; Nav prefixes them with the active locale at render.
9
- // `footerColumns` carries the footer's column groups (#6468), keys only — the
10
- // layout resolves the labels.
3
+ // GENERATED by create-ailk from the declared route surfaces. Every entry here
4
+ // points at a route this site actually carries — the nav is regenerated with
5
+ // the surface set so it can never link a page that was not scaffolded.
11
6
  //
12
- // In a SCAFFOLD this file is regenerated by create-ailk from the declared
13
- // route surfaces (`renderNavConfig` in packages/create-ailk/src/surfaces.ts)
14
- // so the nav never links a page the scaffold does not carry — keep the two in
15
- // step when the base item set changes.
16
- //
17
- // SOFT_LOCK_OK: #6508 — Product's flyout is `width: "panel"` (D4/D5): a fixed
18
- // 640px, two-column grid, replacing the prior "list"/"trigger" one-column
19
- // menu. `presentation` is omitted — it defaults to "grid" (D5). The `docs`
20
- // action is dropped (it duplicated the first feature's /how-it-works
21
- // destination, § 1.4). This note lives here, not inside the `navItems` array
22
- // below, so this file's `navItems`/`footerColumns` blocks stay byte-identical
23
- // to `renderNavConfig`'s generated output — the mirror test (#6468) diffs the
24
- // arrays verbatim.
25
-
7
+ // Adding a link: scaffold the page (the `add-page` skill), then add an
8
+ // entry below with a `labelKey` that exists in the `nav` namespace of
9
+ // `apps/web/messages/<locale>.json`. Paths are locale-agnostic — Nav prefixes
10
+ // them with the active locale at render.
26
11
  import type { NavItem } from "@working-theory/ui";
27
- import { HelpCircle, Layers, Sparkles } from "lucide-react";
28
12
 
29
13
  export const navItems: NavItem[] = [
30
14
  {
@@ -32,19 +16,41 @@ export const navItems: NavItem[] = [
32
16
  flyout: {
33
17
  width: "panel",
34
18
  features: [
35
- { labelKey: "useCases", descKey: "useCasesDesc", icon: Layers, path: "/use-cases" },
36
- { labelKey: "features", descKey: "featuresDesc", icon: Sparkles, path: "/software/product" },
37
- { labelKey: "faq", descKey: "faqDesc", icon: HelpCircle, path: "/faq" },
19
+ { labelKey: "feature1", path: "/features/feature-1" },
20
+ { labelKey: "feature2", path: "/features/feature-2" },
21
+ { labelKey: "feature3", path: "/features/feature-3" },
22
+ { labelKey: "useCase1", path: "/use-cases/persona-1" },
23
+ { labelKey: "useCase2", path: "/use-cases/persona-2" },
24
+ { labelKey: "useCase3", path: "/use-cases/persona-3" },
25
+ ],
26
+ groups: [
27
+ {
28
+ headingKey: "flyoutFeaturesHeading",
29
+ items: [
30
+ { labelKey: "feature1", path: "/features/feature-1" },
31
+ { labelKey: "feature2", path: "/features/feature-2" },
32
+ { labelKey: "feature3", path: "/features/feature-3" },
33
+ ],
34
+ },
35
+ {
36
+ headingKey: "flyoutUseCasesHeading",
37
+ items: [
38
+ { labelKey: "useCase1", path: "/use-cases/persona-1" },
39
+ { labelKey: "useCase2", path: "/use-cases/persona-2" },
40
+ { labelKey: "useCase3", path: "/use-cases/persona-3" },
41
+ ],
42
+ },
38
43
  ],
39
44
  actions: [],
40
45
  },
41
46
  },
42
- { labelKey: "useCases", path: "/use-cases" },
43
47
  { labelKey: "blog", path: "/blog" },
44
48
  { labelKey: "pricing", path: "/pricing" },
45
49
  { labelKey: "about", path: "/about" },
46
50
  ];
47
51
 
52
+ // Footer column groups. Keys only — the layout resolves each `headingKey`
53
+ // and `labelKey` through the same `nav` labels as the header.
48
54
  export interface FooterColumnConfig {
49
55
  headingKey: string;
50
56
  items: { labelKey: string; path: string }[];
@@ -54,9 +60,8 @@ export const footerColumns: FooterColumnConfig[] = [
54
60
  {
55
61
  headingKey: "footerSitePagesHeading",
56
62
  items: [
57
- { labelKey: "features", path: "/software/product" },
63
+ { labelKey: "features", path: "/features" },
58
64
  { labelKey: "pricing", path: "/pricing" },
59
- { labelKey: "useCases", path: "/use-cases" },
60
65
  { labelKey: "about", path: "/about" },
61
66
  { labelKey: "contact", path: "/contact" },
62
67
  { labelKey: "bookACall", path: "/book-a-call" },
@@ -65,12 +70,19 @@ export const footerColumns: FooterColumnConfig[] = [
65
70
  {
66
71
  headingKey: "footerFeaturesHeading",
67
72
  items: [
68
- { labelKey: "feature1", path: "/software/product" },
69
- { labelKey: "feature2", path: "/software/product" },
70
- { labelKey: "feature3", path: "/software/product" },
71
- { labelKey: "feature4", path: "/software/product" },
72
- { labelKey: "feature5", path: "/software/product" },
73
- { labelKey: "allFeatures", path: "/software/product" },
73
+ { labelKey: "feature1", path: "/features/feature-1" },
74
+ { labelKey: "feature2", path: "/features/feature-2" },
75
+ { labelKey: "feature3", path: "/features/feature-3" },
76
+ { labelKey: "allFeatures", path: "/features" },
77
+ ],
78
+ },
79
+ {
80
+ headingKey: "footerUseCasesHeading",
81
+ items: [
82
+ { labelKey: "useCase1", path: "/use-cases/persona-1" },
83
+ { labelKey: "useCase2", path: "/use-cases/persona-2" },
84
+ { labelKey: "useCase3", path: "/use-cases/persona-3" },
85
+ { labelKey: "allUseCases", path: "/use-cases" },
74
86
  ],
75
87
  },
76
88
  {
@@ -85,6 +85,20 @@ function readFeatureList(contentRoot: string, locale: string): string[] {
85
85
  const frontmatter = readFrontmatter(join(productDir, name));
86
86
  if (frontmatter !== undefined) return featuresOf(frontmatter);
87
87
  }
88
+ // #6721 — the Free roster's feature PAGES (one `Feature` seed per feature,
89
+ // #6721) are the featureList when the site carries no product page: each
90
+ // page's title, in sorted file order.
91
+ const featureDir = join(contentRoot, locale, "Feature");
92
+ try {
93
+ const titles = readdirSync(featureDir)
94
+ .filter((name) => name.endsWith(".mdx"))
95
+ .sort()
96
+ .map((name) => readFrontmatter(join(featureDir, name))?.["title"])
97
+ .filter((title): title is string => typeof title === "string" && title.length > 0);
98
+ if (titles.length > 0) return titles;
99
+ } catch {
100
+ // No Feature directory — fall through to the legacy probe.
101
+ }
88
102
  let typeDirs: string[];
89
103
  try {
90
104
  typeDirs = readdirSync(join(contentRoot, locale)).sort();
@@ -38,7 +38,14 @@
38
38
  "useCases": "Use cases",
39
39
  "signUp": "Sign up",
40
40
  "skipToContent": "Skip to content",
41
- "useCasesDesc": "'{One line on how your product works}'"
41
+ "useCasesDesc": "'{One line on how your product works}'",
42
+ "useCase1": "'{Use Case 1}'",
43
+ "useCase2": "'{Use Case 2}'",
44
+ "useCase3": "'{Use Case 3}'",
45
+ "footerUseCasesHeading": "Use Cases",
46
+ "allUseCases": "All use cases",
47
+ "flyoutFeaturesHeading": "Features",
48
+ "flyoutUseCasesHeading": "Use Cases"
42
49
  },
43
50
  "consent": {
44
51
  "title": "Cookie Preferences",
@@ -47,7 +54,7 @@
47
54
  "rejectLabel": "Reject",
48
55
  "policyLinkLabel": "Privacy policy",
49
56
  "dismissLabel": "Close",
50
- "settingsLabel": "Cookie settings"
57
+ "settingsLabel": "Cookie Settings"
51
58
  },
52
59
  "common": {
53
60
  "siteTitle": "My Site",
@@ -79,6 +86,10 @@
79
86
  "heading": "Case Studies",
80
87
  "emptyState": "Case studies listing coming soon."
81
88
  },
89
+ "portfolio": {
90
+ "heading": "Portfolio",
91
+ "emptyState": "Portfolio coming soon."
92
+ },
82
93
  "courses": {
83
94
  "heading": "Courses",
84
95
  "emptyState": "Courses listing coming soon."
@@ -97,6 +108,10 @@
97
108
  "sendMessage": "Send message",
98
109
  "successMessage": "Thank you for reaching out! We will be in touch shortly.",
99
110
  "consentStatement": "I accept the [Terms](terms).",
111
+ "genericError": "Something went wrong, please try again.",
112
+ "unavailableHeading": "Get in touch",
113
+ "unavailableBody": "Email us at {email} and we'll reply.",
114
+ "unavailableNoEmail": "Contact details for this site haven't been published yet.",
100
115
  "intake": {
101
116
  "guidedBadge": "Guided",
102
117
  "skip": "Skip",
@@ -682,5 +697,16 @@
682
697
  "emptyHeadline": "No upcoming bookings",
683
698
  "emptyDescription": "When someone books a time with you, the appointment appears here automatically."
684
699
  }
700
+ },
701
+ "features": {
702
+ "heading": "Features",
703
+ "emptyState": "Features coming soon."
704
+ },
705
+ "newsletter": {
706
+ "emailLabel": "Email",
707
+ "submit": "Subscribe",
708
+ "sending": "Subscribing…",
709
+ "success": "You're subscribed. Thanks for signing up!",
710
+ "consentStatement": "I accept the [Terms](terms)."
685
711
  }
686
712
  }
@@ -3,6 +3,9 @@ version: "1"
3
3
  siteName: "{{COMPANY_NAME}}"
4
4
  baseUrl: https://{{COMPANY_DOMAIN}}
5
5
  defaultLocale: en
6
+ # /contact shows this address instead of the form when the site cannot take
7
+ # messages (no database and no email key in production).
8
+ contactEmail: "hello@{{COMPANY_DOMAIN}}"
6
9
  siteIdentity:
7
10
  kind: Organization
8
11
  name: "{{COMPANY_NAME}}"
@@ -59,8 +62,7 @@ every page's structured data carries it:
59
62
  `name` and `url` (optional `description`, `applicationCategory`,
60
63
  `operatingSystem`). The home page's main entity becomes the product,
61
64
  published by your organization, and its feature list is read from your
62
- product page (`content/en/SoftwareProduct/product.mdx`, served at
63
- `/software/product`).
65
+ feature pages (`content/en/Feature/*.mdx`, served at `/features/<slug>`).
64
66
  - under a Person — the company you run: `kind: Organization` with a `name`
65
67
  and `url` (optional `logo`, `description`, `sameAs`).
66
68