create-ailk 0.28.1 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -1
- package/dist/cli.js +92 -2
- package/dist/lib/import-mdx.d.ts +59 -0
- package/dist/lib/import-mdx.js +461 -0
- package/dist/migrate-content.d.ts +45 -0
- package/dist/migrate-content.js +215 -0
- package/dist/seed-workspace.d.ts +71 -0
- package/dist/seed-workspace.js +242 -0
- package/dist/topology.js +7 -0
- package/package.json +3 -3
- package/templates/apps/api/CLAUDE.md +2 -2
- package/templates/apps/mcp/CLAUDE.md +16 -12
- package/templates/apps/web/CLAUDE.md +11 -1
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +4 -0
- package/templates/apps/web/app/[locale]/alternatives/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/careers/page.tsx +23 -15
- package/templates/apps/web/app/[locale]/clients/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/faq/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/glossary/page.tsx +3 -0
- package/templates/apps/web/app/[locale]/templates/page.tsx +6 -0
- package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
- package/templates/apps/web/lib/hub-children.tsx +38 -9
- package/templates/apps/web/messages/en.json +22 -2
- package/versions.json +48 -48
|
@@ -124,16 +124,18 @@ Every tool maps 1:1 to an `apps/api` endpoint. When you change a tool, change th
|
|
|
124
124
|
**Maintenance (2):** `audit`, `create_project` (stdio only, #9965 D13)
|
|
125
125
|
**Flow (2):** `get_flow`, `submit_flow_step` — module-gated (`module: "signal-collectors"`, D-48, #5366) and licence-feature-tagged (`feature: "flows-deliverables"`, #7136): identity-blind like every tool in this list, registered only when the signal-collectors module is present, and gated on the deployment's `flows-deliverables` licence feature under `MCP_FEATURE_GATE_MODE`, the same shape Booking below has. `submit_flow_step` takes the HTTP write branch (its own daily cap, `MCP_ABUSE_SUBMIT_FLOW_STEP_DAILY_CAP`, default 50 — #8071)
|
|
126
126
|
**Booking (4):** `list_availability`, `get_event_meta`, `list_meetings`, `create_booking` — module-gated (`module: "scheduling"`) and licence-feature-tagged (`feature: "scheduling"`, #7136), gated on the deployment's `scheduling` licence feature under `MCP_FEATURE_GATE_MODE`. `create_booking` takes the HTTP write branch (its own daily cap, `MCP_ABUSE_CREATE_BOOKING_DAILY_CAP`, default 20, and an `attendee.email` format check — #8071). `list_meetings` (#8843 D5) twins `GET /v1/schedule/meetings`: it takes no argument and returns the site's Live meetings — slug, visibility, position and whether each can be booked, plus a bookable one's calLink, title, length, description and price; `private` means unlisted, not secret
|
|
127
|
+
**Redirect (1):** `get_redirect` (#10202 D8) — module-gated (`module: "content-write"`) and licence-feature-tagged (`feature: "workspace"`, the content-write claim), gated on the deployment's `workspace` licence feature under `MCP_FEATURE_GATE_MODE`. It twins `GET /v1/redirects`: given an old site path (and an optional locale) it returns where the site permanently redirects it — `{ from, to, permanent: true }` — or the route's not-found body. Its handler rides `@working-theory/mcp-workspace`'s anonymous `tools` (`redirect.ts`), the pack the `workspace` and `multi-tenant` modules carry, and calls the module-level `apiClient` with no agent key
|
|
127
128
|
|
|
128
|
-
### Why the parity test counts
|
|
129
|
+
### Why the parity test counts 22 and a keyless server registers 20 (21 over stdio once Free is released)
|
|
129
130
|
|
|
130
|
-
- **
|
|
131
|
-
`packages/metadata/src/mcp-tool-catalog.ts` defines
|
|
132
|
-
parity test counts that full array. It also holds the thirty-
|
|
133
|
-
workspace tools (#3591
|
|
131
|
+
- **22 in the `TOOLS` array / parity test**: the `TOOLS` array in
|
|
132
|
+
`packages/metadata/src/mcp-tool-catalog.ts` defines 22 anonymous tools; the
|
|
133
|
+
parity test counts that full array. It also holds the thirty-three signed-in
|
|
134
|
+
workspace tools (#3591; the content-suggestion pair `draft_page_suggestions`
|
|
135
|
+
and `decide_page_suggestion` since #10218), which no anonymous view returns — see § The
|
|
134
136
|
signed-in workspace surface below.
|
|
135
|
-
- **
|
|
136
|
-
while the Free level is held or over HTTP —
|
|
137
|
+
- **20 registered on a KEYLESS deployment, 21 on a commerce-claimed one,
|
|
138
|
+
while the Free level is held or over HTTP — 21 and 22 over stdio once it is
|
|
137
139
|
released** (#5971, #9534, #9965 D13): `createMcpServer` (`apps/mcp/src/server.ts`) registers
|
|
138
140
|
`getMcpToolRegistrations()` minus the topology seam's `UNSERVED_MCP_TOOLS`,
|
|
139
141
|
minus `create_project` while create-ailk's `release-scope.json` holds
|
|
@@ -143,16 +145,16 @@ Every tool maps 1:1 to an `apps/api` endpoint. When you change a tool, change th
|
|
|
143
145
|
`addon`-tagged tool whose claim this deployment does not hold.
|
|
144
146
|
`start_purchase` carries `addon: "commerce"`, so it is the one entry whose
|
|
145
147
|
presence depends on the licence — one fewer without the claim.
|
|
146
|
-
- **
|
|
148
|
+
- **20 anonymous tools while Free is held or over HTTP (21 over stdio once released)**: since #5853 retired the three content-write
|
|
147
149
|
tools, every registered tool remains anonymous — call-time gating checks a
|
|
148
|
-
licence claim, never an identity. The
|
|
150
|
+
licence claim, never an identity. The seven carrying `tier: "pro"`
|
|
149
151
|
(`get_flow`, `submit_flow_step`, `list_availability`, `get_event_meta`,
|
|
150
|
-
`list_meetings`, `create_booking`) are gated at REGISTRATION **and at call time** (#7315):
|
|
152
|
+
`list_meetings`, `create_booking`, `get_redirect`) are gated at REGISTRATION **and at call time** (#7315):
|
|
151
153
|
each needs its module, and each catalog row names the licence `feature:`
|
|
152
154
|
that entitles it. `start_purchase` is gated at call time by both this
|
|
153
155
|
server-wide gate and its own Gate 2 in `tools/purchase.ts`.
|
|
154
156
|
`MCP_FEATURE_GATE_MODE` (`packages/metadata`) decides what an unheld
|
|
155
|
-
feature does. In `"warn"` mode (this release) the
|
|
157
|
+
feature does. In `"warn"` mode (this release) the seven register on module
|
|
156
158
|
presence alone, and the boot (`bootMcp()`) logs one warning (stderr) per tool
|
|
157
159
|
served without its feature. In `"refuse"` mode (the next release) they
|
|
158
160
|
also need the deployment's licence to hold the feature, and
|
|
@@ -283,7 +285,9 @@ Example (lead routing):
|
|
|
283
285
|
- Don't add a tool without the matching `apps/api` endpoint.
|
|
284
286
|
- Don't put business logic in tool handlers — they're adapters.
|
|
285
287
|
|
|
286
|
-
Last updated: 2026-10-
|
|
288
|
+
Last updated: 2026-10-29 — #10202 (D8): `get_redirect` (`mcp-workspace`'s `redirect.ts`, the pack's first anonymous handler) twins `GET /v1/redirects` under `module: "content-write"` and `feature: "workspace"` — 22 in `TOOLS`, 20 registered keyless and 21 commerce-claimed while Free is held, seven `tier: "pro"` — verified against `packages/metadata/src/mcp-tool-catalog.ts`, `packages-paid/mcp-workspace/src/index.ts` and `tests/parity/api-parity/kinds/site.ts`.
|
|
289
|
+
|
|
290
|
+
Prior: 2026-10-26 — #9965 (D12, D13): `create_project` is served over stdio only — the HTTP transport, the default, never registers it, and the web manifest always drops it — and `trustOneHop` trusts its hop only on Vercel — verified against `packages/mcp-server/src/{server,run,serverless-app}.ts` and `apps/web/app/.well-known/mcp.json/route.ts`.
|
|
287
291
|
|
|
288
292
|
Prior: 2026-10-24 — #9749: `get_slack_connection_status` (`mcp-workspace`'s `slack_connection.ts`) and `get_prospect_channel_settings` / `set_prospect_channel_settings` (`mcp-scheduling`'s `prospect_channel_settings.ts`, the first signed-in handlers outside `mcp-workspace`) twin the Slack status read and the prospect-channel settings pair — the signed-in workspace surface is thirty-one tools — verified against `packages/metadata/src/mcp-tool-catalog.ts`, `packages-paid/mcp-workspace/src/index.ts`, `packages-paid/mcp-scheduling/src/index.ts` and `tests/parity/api-parity/kinds/site.ts`.
|
|
289
293
|
|
|
@@ -594,6 +594,14 @@ A site serves its services at `/services` unless `content/_site.mdx` sets `servi
|
|
|
594
594
|
- **Surfaces.** The canonical, hreflang and `BreadcrumbList` (the base's title-cased last segment, then the category's title), `/sitemap.xml` and `/llms.txt` (`pagePathFor`), every nav and footer entry at `/services` (mapped in the locale layout and `components/signed-in-nav.tsx`), the hub's own section links, and route-family chrome (`lib/page-chrome.ts`) all follow it. A site that sets neither `services.path` nor any `parent` serves every URL exactly as before.
|
|
595
595
|
- **Moving it.** `/ailk:growth-ops page rename` moves the base (`--from <current base> --to <new base>`): it writes `services.path` and keeps a previous moved base redirecting.
|
|
596
596
|
|
|
597
|
+
## Nav-only lists (`navOnly`)
|
|
598
|
+
|
|
599
|
+
A list page whose frontmatter sets `navOnly: true` (valid on a registry list type only, #10200) has no page of its own: `honourNavOnly` (`lib/hub-children.tsx`) answers `permanentRedirect` (308) to the first child in the route's own child order (a hub's is slug order after `includeChild`, not the order of links its authored sections carry), at the card's public path with the locale prefix kept, and `notFound()` when it lists none. Only an exact `true` acts; anything else renders as before.
|
|
600
|
+
|
|
601
|
+
- **Where.** The thirteen hubs inside `hubPageInput` (slug order after `includeChild`; `/services` at `servicePublicPath`), `/alternatives` (`/vs/<slug>`, slug-read order), `/templates`, `/careers` (newest first) and `/reports` (newest first) with one call each, and the childless `/faq`, `/glossary`, `/clients`, `/testimonials` and the `[slug]` `BlogList` teaser, which 404.
|
|
602
|
+
- **Not honoured.** The docs-blog module's Fumadocs `/blog` and `/docs`; `DownloadsList` (`/downloads` already redirects to `/reports`); `LocationsList` (no route). `/sitemap.xml` and `/llms.txt` still list a Nav-only list's URL.
|
|
603
|
+
- **The nav is untouched.** `lib/nav-config.ts` is generated from route surfaces, not from list pages, so the heading and its children stay in the menu.
|
|
604
|
+
|
|
597
605
|
## AEO & performance
|
|
598
606
|
|
|
599
607
|
- Static rendering is the default. Don't introduce client rendering without a reason.
|
|
@@ -610,7 +618,9 @@ A site serves its services at `/services` unless `content/_site.mdx` sets `servi
|
|
|
610
618
|
- Don't reach into `apps/api` or `apps/mcp` from `apps/web`. They're peer apps — talk via HTTP/MCP.
|
|
611
619
|
- Don't commit images to `screenshots/` — only the `.gitkeep` is tracked.
|
|
612
620
|
|
|
613
|
-
Last updated: 2026-10-
|
|
621
|
+
Last updated: 2026-10-28 — § Nav-only lists (`navOnly`): a Nav-only list's route answers 308 to its first child and 404 with none (#10200) — verified against `lib/hub-children.tsx` and the nine list routes it is called from beside the hubs.
|
|
622
|
+
|
|
623
|
+
Prior: 2026-10-27 — § Where the services live (`services.path`): the services base, one category level, the 308 from `/services` and the `update-page` base move (#10043) — verified against `lib/services-path.ts`, `lib/service-listing.ts`, `middleware.ts`, `next.config.mjs` and `app/[locale]/services/`.
|
|
614
624
|
|
|
615
625
|
Prior: 2026-10-26 — § The same-origin relay: the bot check moves to `lib/bot-check.ts` and covers the relay's three lead routes and the seven public lead, flow and checkout Server Actions, with `BotIdClient` protecting every same-origin POST and the `AILK_BOT_CHECK` switch (#9255) — verified against `lib/bot-check.ts`, `lib/relay.ts`, `app/[locale]/layout.tsx` and the seven actions' files.
|
|
616
626
|
|
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
import type { Metadata } from "next";
|
|
43
43
|
import type { ReactElement } from "react";
|
|
44
44
|
|
|
45
|
+
import { honourNavOnly } from "../../../lib/hub-children";
|
|
46
|
+
|
|
45
47
|
// The content type is written as a literal in both renderPage calls below
|
|
46
48
|
// (not a shared constant): `lib/__tests__/structured-data.test.tsx` and
|
|
47
49
|
// tools/lint-check-content-renders.ts read `renderPage({ type: "…" })` off
|
|
@@ -87,6 +89,8 @@ export default async function Page({
|
|
|
87
89
|
const load = getMdxSource ?? loadMdxSource;
|
|
88
90
|
const type = await resolveType(slug, locale, load);
|
|
89
91
|
if (type === "BlogList") {
|
|
92
|
+
const source = await load("BlogList", slug, locale);
|
|
93
|
+
honourNavOnly(source?.frontmatter, locale, []);
|
|
90
94
|
return renderPage({
|
|
91
95
|
type: "BlogList",
|
|
92
96
|
slug,
|
|
@@ -27,6 +27,8 @@ import type { Metadata } from "next";
|
|
|
27
27
|
import { notFound } from "next/navigation";
|
|
28
28
|
import type { ReactElement } from "react";
|
|
29
29
|
|
|
30
|
+
import { honourNavOnly } from "../../../lib/hub-children";
|
|
31
|
+
|
|
30
32
|
interface Props {
|
|
31
33
|
params: Promise<{ locale: string }>;
|
|
32
34
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
@@ -60,6 +62,13 @@ export default async function Page({
|
|
|
60
62
|
notFound();
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
const slugs = await getMdxSlugs("ComparisonPage", locale);
|
|
66
|
+
honourNavOnly(
|
|
67
|
+
source.frontmatter,
|
|
68
|
+
locale,
|
|
69
|
+
slugs.map((slug) => `/vs/${slug}`),
|
|
70
|
+
);
|
|
71
|
+
|
|
63
72
|
/**
|
|
64
73
|
* Append one generated `items` section listing every ComparisonPage spoke
|
|
65
74
|
* — D9's "the visible list and the CollectionPage's ItemList always
|
|
@@ -69,7 +78,6 @@ export default async function Page({
|
|
|
69
78
|
*/
|
|
70
79
|
async function withSpokes(frontmatter: unknown): Promise<Section[]> {
|
|
71
80
|
const declared = await loadSections(frontmatter);
|
|
72
|
-
const slugs = await getMdxSlugs("ComparisonPage", locale);
|
|
73
81
|
if (slugs.length === 0) return declared;
|
|
74
82
|
|
|
75
83
|
const items = await Promise.all(
|
|
@@ -25,6 +25,8 @@ import type { Metadata } from "next";
|
|
|
25
25
|
import { getTranslations } from "next-intl/server";
|
|
26
26
|
import type { ReactElement } from "react";
|
|
27
27
|
|
|
28
|
+
import { honourNavOnly } from "../../../lib/hub-children";
|
|
29
|
+
|
|
28
30
|
interface Props {
|
|
29
31
|
params: Promise<{ locale: string }>;
|
|
30
32
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
@@ -75,6 +77,26 @@ export default async function Page({
|
|
|
75
77
|
const loadSections = getPageSections ?? loadPageSections;
|
|
76
78
|
const t = await getTranslations({ locale, namespace: "careers" });
|
|
77
79
|
|
|
80
|
+
const slugs = await listSlugs("JobPosting", locale);
|
|
81
|
+
const postings = await Promise.all(
|
|
82
|
+
slugs.map(async (slug) => {
|
|
83
|
+
const source = await load("JobPosting", slug, locale);
|
|
84
|
+
return { slug, fm: (source?.frontmatter ?? {}) as PostingFields };
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
// Newest `publishedAt` first, then by slug; no date sorts last.
|
|
88
|
+
postings.sort(
|
|
89
|
+
(a, b) =>
|
|
90
|
+
(b.fm.publishedAt ?? "").localeCompare(a.fm.publishedAt ?? "") ||
|
|
91
|
+
a.slug.localeCompare(b.slug),
|
|
92
|
+
);
|
|
93
|
+
const source = await load("CareersList", "careers", locale);
|
|
94
|
+
honourNavOnly(
|
|
95
|
+
source?.frontmatter,
|
|
96
|
+
locale,
|
|
97
|
+
postings.map(({ slug }) => `/careers/${slug}`),
|
|
98
|
+
);
|
|
99
|
+
|
|
78
100
|
/**
|
|
79
101
|
* Put one generated `items` section first — the postings are the page's
|
|
80
102
|
* main content — followed by the declared sections. Absent when there are
|
|
@@ -82,21 +104,7 @@ export default async function Page({
|
|
|
82
104
|
*/
|
|
83
105
|
async function withPostings(frontmatter: unknown): Promise<Section[]> {
|
|
84
106
|
const declared = await loadSections(frontmatter);
|
|
85
|
-
|
|
86
|
-
if (slugs.length === 0) return declared;
|
|
87
|
-
|
|
88
|
-
const postings = await Promise.all(
|
|
89
|
-
slugs.map(async (slug) => {
|
|
90
|
-
const source = await load("JobPosting", slug, locale);
|
|
91
|
-
return { slug, fm: (source?.frontmatter ?? {}) as PostingFields };
|
|
92
|
-
}),
|
|
93
|
-
);
|
|
94
|
-
// Newest `publishedAt` first, then by slug; no date sorts last.
|
|
95
|
-
postings.sort(
|
|
96
|
-
(a, b) =>
|
|
97
|
-
(b.fm.publishedAt ?? "").localeCompare(a.fm.publishedAt ?? "") ||
|
|
98
|
-
a.slug.localeCompare(b.slug),
|
|
99
|
-
);
|
|
107
|
+
if (postings.length === 0) return declared;
|
|
100
108
|
|
|
101
109
|
const items = postings.map(({ slug, fm }) => {
|
|
102
110
|
const type =
|
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
import type {
|
|
4
4
|
getMdxSource as defaultGetMdxSource,
|
|
5
5
|
} from "@working-theory/page-renderer";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getMdxSource as loadMdxSource,
|
|
8
|
+
renderPage,
|
|
9
|
+
renderPageMetadata,
|
|
10
|
+
} from "@working-theory/page-renderer";
|
|
7
11
|
import type { Metadata } from "next";
|
|
8
12
|
import type { ReactElement } from "react";
|
|
9
13
|
|
|
14
|
+
import { honourNavOnly } from "../../../lib/hub-children";
|
|
15
|
+
|
|
10
16
|
interface Props {
|
|
11
17
|
params: Promise<{ locale: string }>;
|
|
12
18
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
@@ -24,6 +30,12 @@ export default async function Page({
|
|
|
24
30
|
getMdxSource,
|
|
25
31
|
}: Props): Promise<ReactElement> {
|
|
26
32
|
const { locale } = await params;
|
|
33
|
+
const source = await (getMdxSource ?? loadMdxSource)(
|
|
34
|
+
"ClientsList",
|
|
35
|
+
"clients",
|
|
36
|
+
locale,
|
|
37
|
+
);
|
|
38
|
+
honourNavOnly(source?.frontmatter, locale, []);
|
|
27
39
|
return renderPage({
|
|
28
40
|
type: "ClientsList",
|
|
29
41
|
slug: "clients",
|
|
@@ -5,12 +5,15 @@ import type {
|
|
|
5
5
|
getMdxSource as defaultGetMdxSource,
|
|
6
6
|
getPageSections as defaultGetPageSections} from "@working-theory/page-renderer";
|
|
7
7
|
import {
|
|
8
|
+
getMdxSource as loadMdxSource,
|
|
8
9
|
renderPage,
|
|
9
10
|
renderPageMetadata
|
|
10
11
|
} from "@working-theory/page-renderer";
|
|
11
12
|
import type { Metadata } from "next";
|
|
12
13
|
import type { ReactElement } from "react";
|
|
13
14
|
|
|
15
|
+
import { honourNavOnly } from "../../../lib/hub-children";
|
|
16
|
+
|
|
14
17
|
interface Props {
|
|
15
18
|
params: Promise<{ locale: string }>;
|
|
16
19
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
@@ -30,6 +33,12 @@ export default async function Page({
|
|
|
30
33
|
getPageSections,
|
|
31
34
|
}: Props): Promise<ReactElement> {
|
|
32
35
|
const { locale } = await params;
|
|
36
|
+
const source = await (getMdxSource ?? loadMdxSource)(
|
|
37
|
+
"FAQPage",
|
|
38
|
+
"faq",
|
|
39
|
+
locale,
|
|
40
|
+
);
|
|
41
|
+
honourNavOnly(source?.frontmatter, locale, []);
|
|
33
42
|
return renderPage({
|
|
34
43
|
type: "FAQPage",
|
|
35
44
|
slug: "faq",
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
import type { Metadata } from "next";
|
|
14
14
|
import type { ReactElement } from "react";
|
|
15
15
|
|
|
16
|
+
import { honourNavOnly } from "../../../lib/hub-children";
|
|
17
|
+
|
|
16
18
|
interface Props {
|
|
17
19
|
params: Promise<{ locale: string }>;
|
|
18
20
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
@@ -35,6 +37,7 @@ export default async function Page({
|
|
|
35
37
|
}: Props): Promise<ReactElement> {
|
|
36
38
|
const { locale } = await params;
|
|
37
39
|
const source = await (getMdxSource ?? loadMdxSource)("GlossaryList", "glossary", locale);
|
|
40
|
+
honourNavOnly(source?.frontmatter, locale, []);
|
|
38
41
|
const composed =
|
|
39
42
|
source !== null &&
|
|
40
43
|
Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
|
|
@@ -27,6 +27,7 @@ import { notFound } from "next/navigation";
|
|
|
27
27
|
import { getTranslations } from "next-intl/server";
|
|
28
28
|
import type { ReactElement } from "react";
|
|
29
29
|
|
|
30
|
+
import { honourNavOnly } from "../../../lib/hub-children";
|
|
30
31
|
import {
|
|
31
32
|
insertBeforeClosingCtas,
|
|
32
33
|
listDetailPages,
|
|
@@ -67,6 +68,11 @@ export default async function Page({
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
const pages = await listDetailPages("TemplatePage", locale, load);
|
|
71
|
+
honourNavOnly(
|
|
72
|
+
source.frontmatter,
|
|
73
|
+
locale,
|
|
74
|
+
pages.map((page) => `/templates/${page.slug}`),
|
|
75
|
+
);
|
|
70
76
|
const t = await getTranslations({ namespace: "templates", locale });
|
|
71
77
|
const labels = templateLabels(t);
|
|
72
78
|
|
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
import type {
|
|
4
4
|
getMdxSource as defaultGetMdxSource,
|
|
5
5
|
} from "@working-theory/page-renderer";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getMdxSource as loadMdxSource,
|
|
8
|
+
renderPage,
|
|
9
|
+
renderPageMetadata,
|
|
10
|
+
} from "@working-theory/page-renderer";
|
|
7
11
|
import type { Metadata } from "next";
|
|
8
12
|
import type { ReactElement } from "react";
|
|
9
13
|
|
|
14
|
+
import { honourNavOnly } from "../../../lib/hub-children";
|
|
15
|
+
|
|
10
16
|
interface Props {
|
|
11
17
|
params: Promise<{ locale: string }>;
|
|
12
18
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
@@ -28,6 +34,12 @@ export default async function Page({
|
|
|
28
34
|
getMdxSource,
|
|
29
35
|
}: Props): Promise<ReactElement> {
|
|
30
36
|
const { locale } = await params;
|
|
37
|
+
const source = await (getMdxSource ?? loadMdxSource)(
|
|
38
|
+
"TestimonialsList",
|
|
39
|
+
"testimonials",
|
|
40
|
+
locale,
|
|
41
|
+
);
|
|
42
|
+
honourNavOnly(source?.frontmatter, locale, []);
|
|
31
43
|
return renderPage({
|
|
32
44
|
type: "TestimonialsList",
|
|
33
45
|
slug: "testimonials",
|
|
@@ -30,7 +30,9 @@ import {
|
|
|
30
30
|
getMdxSource as loadMdxSource,
|
|
31
31
|
getPageSections as loadPageSections,
|
|
32
32
|
} from "@working-theory/page-renderer";
|
|
33
|
+
import { localeHref } from "@working-theory/ui";
|
|
33
34
|
import { SectionRenderer } from "@working-theory/ui/sections";
|
|
35
|
+
import { notFound, permanentRedirect } from "next/navigation";
|
|
34
36
|
|
|
35
37
|
import {
|
|
36
38
|
type DetailPage,
|
|
@@ -242,6 +244,26 @@ function hasSections(frontmatter: unknown): boolean {
|
|
|
242
244
|
return Array.isArray(sections) && sections.length > 0;
|
|
243
245
|
}
|
|
244
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Nav only (#10200): a list that is a heading in the site navigation with no
|
|
249
|
+
* page of its own answers a permanent redirect to its first child — in the
|
|
250
|
+
* route's own child order — and 404 when it lists none. Any other page
|
|
251
|
+
* returns, and renders as before.
|
|
252
|
+
*/
|
|
253
|
+
export function honourNavOnly(
|
|
254
|
+
frontmatter: unknown,
|
|
255
|
+
locale: string,
|
|
256
|
+
childHrefs: readonly string[],
|
|
257
|
+
): void {
|
|
258
|
+
if ((frontmatter as { navOnly?: unknown } | undefined)?.navOnly !== true)
|
|
259
|
+
return;
|
|
260
|
+
const first = childHrefs[0];
|
|
261
|
+
if (first === undefined) notFound();
|
|
262
|
+
permanentRedirect(
|
|
263
|
+
localeHref(locale, first, process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? "en"),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
245
267
|
type RenderPageInput = Parameters<typeof renderPage>[0];
|
|
246
268
|
|
|
247
269
|
/**
|
|
@@ -279,25 +301,32 @@ export async function hubPageInput(
|
|
|
279
301
|
};
|
|
280
302
|
}
|
|
281
303
|
|
|
282
|
-
const
|
|
283
|
-
source.frontmatter,
|
|
284
|
-
);
|
|
285
|
-
const pages = await listDetailPages(
|
|
304
|
+
const listed = await listDetailPages(
|
|
286
305
|
config.childType,
|
|
287
306
|
locale,
|
|
288
307
|
load,
|
|
289
308
|
loaders.getMdxSlugs ?? loadMdxSlugs,
|
|
290
309
|
);
|
|
310
|
+
const pages = config.includeChild
|
|
311
|
+
? listed.filter(config.includeChild)
|
|
312
|
+
: listed;
|
|
313
|
+
honourNavOnly(
|
|
314
|
+
source.frontmatter,
|
|
315
|
+
locale,
|
|
316
|
+
pages.flatMap((page) => {
|
|
317
|
+
const card = config.card?.(page) ?? defaultCard(page, config);
|
|
318
|
+
return card.link === undefined ? [] : [card.link.href];
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
const declared = await (loaders.getPageSections ?? loadPageSections)(
|
|
322
|
+
source.frontmatter,
|
|
323
|
+
);
|
|
291
324
|
// A prose hub renders its MDX body, never its declared sections, so its
|
|
292
325
|
// existing links are read from the body and its cards stand alone after it.
|
|
293
326
|
const linked = mdxBody
|
|
294
327
|
? bodyLinkPaths(source.body)
|
|
295
328
|
: linkedPaths(declared);
|
|
296
|
-
const generated = childCardsSection(
|
|
297
|
-
linked,
|
|
298
|
-
config.includeChild ? pages.filter(config.includeChild) : pages,
|
|
299
|
-
config,
|
|
300
|
-
);
|
|
329
|
+
const generated = childCardsSection(linked, pages, config);
|
|
301
330
|
const placed =
|
|
302
331
|
generated === null
|
|
303
332
|
? declared
|
|
@@ -1396,7 +1396,6 @@
|
|
|
1396
1396
|
"notFoundHeadline": "Page not found",
|
|
1397
1397
|
"notFoundDescription": "There is no page at this address. It may have been renamed or removed.",
|
|
1398
1398
|
"editor": {
|
|
1399
|
-
"tabPreview": "Preview",
|
|
1400
1399
|
"toolChanges": "Show unsaved changes",
|
|
1401
1400
|
"backLabel": "Pages",
|
|
1402
1401
|
"statusDraft": "Draft",
|
|
@@ -1427,6 +1426,26 @@
|
|
|
1427
1426
|
"toolNumberedList": "Numbered list",
|
|
1428
1427
|
"toolLink": "Link",
|
|
1429
1428
|
"toolImage": "Insert image",
|
|
1429
|
+
"toolView": "View",
|
|
1430
|
+
"toolRendered": "Rendered — how readers see it",
|
|
1431
|
+
"toolCode": "Markdown — the words with their marks",
|
|
1432
|
+
"toolInsert": "Insert from your library",
|
|
1433
|
+
"insertMeeting": "Add meeting",
|
|
1434
|
+
"insertMagnet": "Add magnet",
|
|
1435
|
+
"insertOffer": "Add offer",
|
|
1436
|
+
"insertAsset": "Add asset",
|
|
1437
|
+
"tips": {
|
|
1438
|
+
"h1": "Your page title is the H1. Use H2 for sections.",
|
|
1439
|
+
"h2": "Types ## before the line → a section heading.",
|
|
1440
|
+
"h3": "Types ### before the line → a smaller heading.",
|
|
1441
|
+
"bold": "Types **word** → bold.",
|
|
1442
|
+
"italic": "Types *word* → italic.",
|
|
1443
|
+
"bulletList": "Types - before each line → a bulleted list.",
|
|
1444
|
+
"numberedList": "Types 1. before each line → a numbered list.",
|
|
1445
|
+
"link": "Types [words](address) → a link.",
|
|
1446
|
+
"insert": "A meeting, magnet, offer or image from your library. AI uses these when it lays out the page.",
|
|
1447
|
+
"changes": "Compare your unsaved edits with the last save."
|
|
1448
|
+
},
|
|
1430
1449
|
"issueRequired": "{field} is required.",
|
|
1431
1450
|
"issueInvalid": "{field} isn't a valid choice.",
|
|
1432
1451
|
"forYou": {
|
|
@@ -1455,7 +1474,8 @@
|
|
|
1455
1474
|
"imageInsertError": "Could not upload that image. Try again.",
|
|
1456
1475
|
"diffLegendAdded": "Added",
|
|
1457
1476
|
"diffLegendRemoved": "Removed",
|
|
1458
|
-
"diffLegendUnchanged": "Unchanged"
|
|
1477
|
+
"diffLegendUnchanged": "Unchanged",
|
|
1478
|
+
"tagPlaceholder": "{kind}: {slug}"
|
|
1459
1479
|
},
|
|
1460
1480
|
"sections": {
|
|
1461
1481
|
"heading": "Sections",
|
package/versions.json
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@working-theory/admin-ui": "0.
|
|
3
|
-
"@working-theory/aeo": "0.
|
|
4
|
-
"@working-theory/agent-context": "0.
|
|
2
|
+
"@working-theory/admin-ui": "0.52.0",
|
|
3
|
+
"@working-theory/aeo": "0.52.0",
|
|
4
|
+
"@working-theory/agent-context": "0.52.0",
|
|
5
5
|
"@working-theory/agent-contract": "0.1.0",
|
|
6
6
|
"@working-theory/agent-memory": "0.0.0",
|
|
7
7
|
"@working-theory/ai": "0.1.0",
|
|
8
|
-
"@working-theory/analytics": "0.
|
|
9
|
-
"@working-theory/api": "0.
|
|
10
|
-
"@working-theory/api-client": "0.
|
|
8
|
+
"@working-theory/analytics": "0.52.0",
|
|
9
|
+
"@working-theory/api": "0.52.0",
|
|
10
|
+
"@working-theory/api-client": "0.52.0",
|
|
11
11
|
"@working-theory/cda-filesystem": "0.0.0",
|
|
12
|
-
"@working-theory/cli": "0.
|
|
13
|
-
"@working-theory/common": "0.
|
|
12
|
+
"@working-theory/cli": "0.52.0",
|
|
13
|
+
"@working-theory/common": "0.52.0",
|
|
14
14
|
"@working-theory/consent": "0.1.0",
|
|
15
|
-
"@working-theory/content-adapters": "0.
|
|
16
|
-
"@working-theory/content-projection": "0.
|
|
17
|
-
"@working-theory/email": "0.
|
|
15
|
+
"@working-theory/content-adapters": "0.52.0",
|
|
16
|
+
"@working-theory/content-projection": "0.52.0",
|
|
17
|
+
"@working-theory/email": "0.52.0",
|
|
18
18
|
"@working-theory/env": "0.0.0",
|
|
19
|
-
"@working-theory/eslint-config": "0.
|
|
20
|
-
"@working-theory/fumadocs-schema": "0.
|
|
21
|
-
"@working-theory/ids": "0.
|
|
22
|
-
"@working-theory/jsonld": "0.
|
|
23
|
-
"@working-theory/license": "0.
|
|
24
|
-
"@working-theory/mcp-server": "0.
|
|
25
|
-
"@working-theory/metadata": "0.
|
|
26
|
-
"@working-theory/observability": "0.
|
|
27
|
-
"@working-theory/page-renderer": "0.
|
|
19
|
+
"@working-theory/eslint-config": "0.52.0",
|
|
20
|
+
"@working-theory/fumadocs-schema": "0.52.0",
|
|
21
|
+
"@working-theory/ids": "0.17.0",
|
|
22
|
+
"@working-theory/jsonld": "0.52.0",
|
|
23
|
+
"@working-theory/license": "0.52.0",
|
|
24
|
+
"@working-theory/mcp-server": "0.52.0",
|
|
25
|
+
"@working-theory/metadata": "0.52.0",
|
|
26
|
+
"@working-theory/observability": "0.52.0",
|
|
27
|
+
"@working-theory/page-renderer": "0.52.0",
|
|
28
28
|
"@working-theory/plugin-mcp": "0.1.3",
|
|
29
|
-
"@working-theory/schema": "0.
|
|
30
|
-
"@working-theory/schema-pack-registry": "0.
|
|
31
|
-
"@working-theory/schema-validator": "0.
|
|
32
|
-
"@working-theory/sections-registry": "0.
|
|
33
|
-
"@working-theory/site-config": "0.
|
|
34
|
-
"@working-theory/spoke-checkout": "0.7.
|
|
35
|
-
"@working-theory/stripe-client": "0.
|
|
36
|
-
"@working-theory/templates": "0.
|
|
37
|
-
"@working-theory/theme": "0.
|
|
38
|
-
"@working-theory/ui": "0.
|
|
39
|
-
"@working-theory/validation": "0.
|
|
40
|
-
"@working-theory/web-i18n": "0.
|
|
41
|
-
"@working-theory/admin": "0.
|
|
42
|
-
"@working-theory/api-commerce": "0.1.
|
|
43
|
-
"@working-theory/api-content-write": "0.
|
|
44
|
-
"@working-theory/api-hub": "0.3.
|
|
45
|
-
"@working-theory/api-multi-tenant": "0.1
|
|
46
|
-
"@working-theory/api-scheduling": "0.4.
|
|
47
|
-
"@working-theory/api-signal-collectors": "0.2.
|
|
48
|
-
"@working-theory/api-workspace": "0.3.
|
|
29
|
+
"@working-theory/schema": "0.52.0",
|
|
30
|
+
"@working-theory/schema-pack-registry": "0.52.0",
|
|
31
|
+
"@working-theory/schema-validator": "0.52.0",
|
|
32
|
+
"@working-theory/sections-registry": "0.52.0",
|
|
33
|
+
"@working-theory/site-config": "0.52.0",
|
|
34
|
+
"@working-theory/spoke-checkout": "0.7.8",
|
|
35
|
+
"@working-theory/stripe-client": "0.52.0",
|
|
36
|
+
"@working-theory/templates": "0.52.0",
|
|
37
|
+
"@working-theory/theme": "0.52.0",
|
|
38
|
+
"@working-theory/ui": "0.52.0",
|
|
39
|
+
"@working-theory/validation": "0.52.0",
|
|
40
|
+
"@working-theory/web-i18n": "0.52.0",
|
|
41
|
+
"@working-theory/admin": "0.8.1",
|
|
42
|
+
"@working-theory/api-commerce": "0.1.6",
|
|
43
|
+
"@working-theory/api-content-write": "0.3.0",
|
|
44
|
+
"@working-theory/api-hub": "0.3.3",
|
|
45
|
+
"@working-theory/api-multi-tenant": "0.2.1",
|
|
46
|
+
"@working-theory/api-scheduling": "0.4.3",
|
|
47
|
+
"@working-theory/api-signal-collectors": "0.2.5",
|
|
48
|
+
"@working-theory/api-workspace": "0.3.2",
|
|
49
49
|
"@working-theory/billing-client": "0.0.0",
|
|
50
50
|
"@working-theory/branding": "0.0.0",
|
|
51
51
|
"@working-theory/license-client": "0.0.0",
|
|
52
52
|
"@working-theory/license-manifest": "0.0.0",
|
|
53
|
-
"@working-theory/mcp-scheduling": "0.3.
|
|
54
|
-
"@working-theory/mcp-signal-collectors": "0.2.
|
|
55
|
-
"@working-theory/mcp-workspace": "0.
|
|
53
|
+
"@working-theory/mcp-scheduling": "0.3.5",
|
|
54
|
+
"@working-theory/mcp-signal-collectors": "0.2.6",
|
|
55
|
+
"@working-theory/mcp-workspace": "0.4.1",
|
|
56
56
|
"@working-theory/partner-session": "0.0.0",
|
|
57
57
|
"@working-theory/preview-protocol": "0.0.0",
|
|
58
|
-
"@working-theory/ui-commerce": "0.23.
|
|
59
|
-
"@working-theory/ui-docs-blog": "0.23.
|
|
60
|
-
"@working-theory/ui-scheduling": "0.25.
|
|
61
|
-
"@working-theory/ui-signal-collectors": "0.27.
|
|
58
|
+
"@working-theory/ui-commerce": "0.23.30",
|
|
59
|
+
"@working-theory/ui-docs-blog": "0.23.30",
|
|
60
|
+
"@working-theory/ui-scheduling": "0.25.4",
|
|
61
|
+
"@working-theory/ui-signal-collectors": "0.27.8",
|
|
62
62
|
"@working-theory/web": "0.0.0",
|
|
63
63
|
"@working-theory/app-api": "1.0.0",
|
|
64
64
|
"@working-theory/mcp": "0.0.0",
|
|
65
|
-
"@working-theory/database": "0.
|
|
65
|
+
"@working-theory/database": "0.52.0"
|
|
66
66
|
}
|