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.
- package/README.md +44 -10
- package/component-catalog.json +520 -5
- package/component-catalog.md +9 -6
- package/dist/cli.js +22 -24
- package/dist/lib/extract-module-bundle.js +5 -1
- package/dist/lib/module-license-gate.d.ts +79 -0
- package/dist/lib/module-license-gate.js +160 -12
- package/dist/lib/module-tier.d.ts +45 -16
- package/dist/lib/module-tier.js +76 -0
- package/dist/lib/pro-callout.js +10 -2
- package/dist/parse-args.d.ts +78 -10
- package/dist/parse-args.js +204 -14
- package/dist/programmatic.d.ts +39 -9
- package/dist/programmatic.js +163 -25
- package/dist/site-roster.js +11 -6
- package/dist/starter-grid.d.ts +216 -0
- package/dist/starter-grid.js +449 -0
- package/dist/surfaces.d.ts +4 -4
- package/dist/surfaces.js +102 -43
- package/dist/template-manifest.d.ts +36 -17
- package/dist/template-manifest.js +121 -367
- package/dist/template-names.d.ts +14 -12
- package/dist/template-names.js +13 -17
- package/package.json +3 -3
- package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -1
- package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
- package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
- package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
- package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
- package/site-templates/designs/standard/DESIGN.md +5 -0
- package/site-templates/marketing-site/DESIGN.md +5 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +89 -25
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +35 -14
- package/site-templates/marketing-site-free/DESIGN.md +5 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +95 -29
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +74 -44
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +17 -29
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -1
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
- package/site-templates/saas-product/DESIGN.md +5 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
- package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +96 -76
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
- package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
- package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
- package/site-templates/shapes/local-business-service/provenance.json +4 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
- package/site-templates/shapes/organization-service/ailk-template.json +10 -0
- package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/shapes/organization-service/provenance.json +4 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
- package/site-templates/shapes/person-service/ailk-template.json +10 -0
- package/site-templates/shapes/person-service/content/_site.mdx +85 -0
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
- package/site-templates/shapes/person-service/provenance.json +4 -0
- package/templates/.claude/skills/README.md +3 -2
- package/templates/.claude/skills/add-page/references/page-types.md +4 -0
- package/templates/.claude/skills/setup-database/SKILL.md +156 -0
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
- package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
- package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +19 -28
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
- package/templates/DESIGN.md +5 -0
- package/templates/README.md +46 -38
- package/templates/apps/api/CLAUDE.md +3 -2
- package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
- package/templates/apps/api/src/config/index.ts +35 -16
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
- package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
- package/templates/apps/api/src/openapi/spec.ts +63 -12
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
- package/templates/apps/api/src/routes/leads/index.ts +46 -1
- package/templates/apps/api/src/server.ts +7 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
- package/templates/apps/api/src/services/lead-routing.ts +140 -30
- package/templates/apps/api/src/services/leads-inbox.ts +48 -11
- package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
- package/templates/apps/mcp/src/tools/create_project.ts +21 -6
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
- package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
- package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/layout.tsx +37 -5
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
- package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +18 -3
- package/templates/apps/web/lib/nav-config.ts +48 -36
- package/templates/apps/web/lib/site-identity.ts +14 -0
- package/templates/apps/web/messages/en.json +28 -2
- package/templates/content/_site.mdx +4 -2
- package/templates/content/en/BlogList/blog.mdx +37 -16
- package/templates/content/en/ContactPage/contact.mdx +14 -44
- package/templates/content/en/HomePage/home.mdx +8 -16
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
- package/templates/content/en/UseCasesList/use-cases.mdx +6 -6
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +1 -1
- package/versions.json +33 -33
- package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
- package/site-templates/simple-service/DESIGN.md +0 -162
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
- package/site-templates/simple-service/ailk-template.json +0 -11
- package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
- package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
- package/site-templates/simple-service/provenance.json +0 -4
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
- package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
- 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.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
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
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
(
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
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
|
-
|
|
45
|
-
|
|
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 —
|
|
2
|
-
// #6508).
|
|
1
|
+
// nav-config.ts — site-nav registry.
|
|
3
2
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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: "
|
|
36
|
-
{ labelKey: "
|
|
37
|
-
{ labelKey: "
|
|
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: "/
|
|
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: "/
|
|
69
|
-
{ labelKey: "feature2", path: "/
|
|
70
|
-
{ labelKey: "feature3", path: "/
|
|
71
|
-
{ labelKey: "
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|