minutework 0.1.6 → 0.1.8
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/EXTERNAL_ALPHA.md +18 -1
- package/README.md +9 -2
- package/assets/claude-local/CLAUDE.md.template +22 -2
- package/assets/claude-local/skills/README.md +9 -0
- package/assets/claude-local/skills/app-pack-authoring.md +5 -1
- package/assets/claude-local/skills/content-structure-and-sections.md +15 -0
- package/assets/claude-local/skills/published-web-and-mw-core-site.md +17 -0
- package/assets/claude-local/skills/schema-engine.md +4 -0
- package/assets/claude-local/skills/secrets-runtime-bridge.md +3 -0
- package/assets/claude-local/skills/sidecar-generation.md +4 -0
- package/assets/templates/fastapi-sidecar/template.schema.json +2 -1
- package/assets/templates/next-tenant-app/.storybook/main.ts +1 -1
- package/assets/templates/next-tenant-app/README.md +20 -3
- package/assets/templates/next-tenant-app/next.config.mjs +33 -0
- package/assets/templates/next-tenant-app/package.json +3 -1
- package/assets/templates/next-tenant-app/public/.gitkeep +1 -0
- package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +73 -0
- package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +5 -3
- package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.test.ts +38 -0
- package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx +145 -0
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +2 -2
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +47 -1
- package/assets/templates/next-tenant-app/src/lib/content/contracts.test.ts +138 -0
- package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +59 -0
- package/assets/templates/next-tenant-app/src/lib/content/release-manifest.test.ts +70 -0
- package/assets/templates/next-tenant-app/src/lib/content/release-manifest.ts +106 -0
- package/assets/templates/next-tenant-app/template.json +2 -2
- package/assets/templates/next-tenant-app/template.schema.json +2 -1
- package/assets/templates/next-tenant-app/tools/template/with-public-site-fixture.mjs +130 -0
- package/dist/agent.d.ts +19 -0
- package/dist/agent.js +308 -0
- package/dist/agent.js.map +1 -0
- package/dist/developer-client.d.ts +59 -0
- package/dist/developer-client.js +35 -0
- package/dist/developer-client.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/assets/templates/next-tenant-app/next.config.ts +0 -18
package/EXTERNAL_ALPHA.md
CHANGED
|
@@ -15,6 +15,10 @@ npx minutework link
|
|
|
15
15
|
|
|
16
16
|
From there, the current external alpha supports two post-link lanes.
|
|
17
17
|
|
|
18
|
+
The shipped `tenant-app` starter is the combined web surface: public routes at
|
|
19
|
+
the root, a private `/app` workspace, and a MinuteWork-managed public-content
|
|
20
|
+
path over the runtime-baseline `mw.core.site` contract.
|
|
21
|
+
|
|
18
22
|
### Developer-local broker lane
|
|
19
23
|
|
|
20
24
|
Use `minutework session` when you want MinuteWork to assemble the workspace
|
|
@@ -54,6 +58,12 @@ The broker fails closed when a live session is already active, the selected
|
|
|
54
58
|
engine is missing, a prior running record has gone stale, or the launch is
|
|
55
59
|
interrupted or fails mid-session.
|
|
56
60
|
|
|
61
|
+
Generated workspaces receive a root `CLAUDE.md` plus exported `skills/`
|
|
62
|
+
guidance so the local coding agent sees the combined-web, snapshot-delivery,
|
|
63
|
+
and `mw.core.site` baseline workflow.
|
|
64
|
+
|
|
65
|
+
Runtime-only Claude hooks remain excluded from the generated workspace export.
|
|
66
|
+
|
|
57
67
|
### Local preview and deploy lane
|
|
58
68
|
|
|
59
69
|
Use the existing local preview/test loop and hosted preview deploy lane when
|
|
@@ -89,10 +99,15 @@ If you run `minutework init` **without** `--starter` in an interactive terminal,
|
|
|
89
99
|
The generated `tenant-app/.env.example` defaults to:
|
|
90
100
|
|
|
91
101
|
```dotenv
|
|
102
|
+
MW_CONTENT_API_TOKEN=
|
|
92
103
|
MW_PUBLIC_SITE_PROPERTY_KEY=main-site
|
|
93
104
|
MW_PUBLIC_SITE_ENV=preview
|
|
94
105
|
```
|
|
95
106
|
|
|
107
|
+
`MW_CONTENT_API_TOKEN` stays server-only. `MW_PUBLIC_SITE_ENV=preview` targets
|
|
108
|
+
preview or draft-safe reads, while `MW_PUBLIC_SITE_ENV=live` is publication-safe
|
|
109
|
+
and intended for published snapshot delivery.
|
|
110
|
+
|
|
96
111
|
If the workspace cannot compile hosted preview release metadata for the linked property, deploy fails closed.
|
|
97
112
|
|
|
98
113
|
## What `deploy --preview` does
|
|
@@ -136,4 +151,6 @@ The CLI does not fabricate success. If the backend cannot materialize a hosted p
|
|
|
136
151
|
- Package versioning stays manual for the first alpha.
|
|
137
152
|
- Publish with npm dist-tag `alpha`.
|
|
138
153
|
- Use the manual GitHub Actions workflow at `.github/workflows/minutework-cli-alpha-publish.yml`.
|
|
139
|
-
-
|
|
154
|
+
- Treat `pnpm --dir packages/minutework-cli smoke:starter` as a hard publish
|
|
155
|
+
gate alongside package tests and build, because the workflow does not run it
|
|
156
|
+
automatically.
|
package/README.md
CHANGED
|
@@ -12,6 +12,10 @@ The current external alpha is intentionally narrow:
|
|
|
12
12
|
- Deploy surface: `minutework deploy --preview`
|
|
13
13
|
- Deferred: `--live`, additional local coding engines, sidecar/runtime-backed deploys, marketplace publish flows
|
|
14
14
|
|
|
15
|
+
The shipped `tenant-app` starter is the combined web surface: public routes at
|
|
16
|
+
the root plus a private `/app` workspace. Public-site content follows the
|
|
17
|
+
MinuteWork-managed `mw.core.site` baseline and published-snapshot flow.
|
|
18
|
+
|
|
15
19
|
The CLI fails closed when the backend cannot provide typed release metadata, deploy status, receipts, activation state, or rollback state. It does not claim a successful deploy when the provider substrate is unavailable.
|
|
16
20
|
|
|
17
21
|
## Install
|
|
@@ -70,6 +74,9 @@ minutework session resume
|
|
|
70
74
|
- The broker fails closed on live overlap, missing engine binaries, stale
|
|
71
75
|
session records, interrupts, and launch failures instead of allowing
|
|
72
76
|
overlapping phantom sessions.
|
|
77
|
+
- Generated workspaces receive a root `CLAUDE.md` plus exported `skills/`
|
|
78
|
+
guidance tailored to the combined web and published-site workflow.
|
|
79
|
+
- Runtime-only Claude hooks are not exported into the generated workspace.
|
|
73
80
|
|
|
74
81
|
### Local preview and deploy lane
|
|
75
82
|
|
|
@@ -84,9 +91,9 @@ minutework deploy --preview
|
|
|
84
91
|
|
|
85
92
|
Workspaces created with **both** `tenant-app` and `sidecar` run `poetry install` in `sidecar` automatically on `pnpm install` (requires [Poetry](https://python-poetry.org/) on your `PATH`). Sidecar-only scaffolds have no root `package.json`; run `poetry install` in `sidecar` once before `minutework dev`.
|
|
86
93
|
|
|
87
|
-
`link` provisions or resolves the default published-site property for the active tenant and stores that property key in repo-local state. The generated `tenant-app/.env.example` defaults to `MW_PUBLIC_SITE_PROPERTY_KEY=main-site` and `MW_PUBLIC_SITE_ENV=preview`.
|
|
94
|
+
`link` provisions or resolves the default published-site property for the active tenant and stores that property key in repo-local state. The generated `tenant-app/.env.example` includes the standard site env contract: `MW_CONTENT_API_TOKEN`, `MW_PUBLIC_SITE_PROPERTY_KEY`, and `MW_PUBLIC_SITE_ENV`. It defaults to `MW_PUBLIC_SITE_PROPERTY_KEY=main-site` and `MW_PUBLIC_SITE_ENV=preview`.
|
|
88
95
|
|
|
89
|
-
`deploy --preview` always revalidates and recompiles before submit, prints a local-vs-remote diff, requires confirmation unless `--yes` is passed, polls typed receipts until a terminal state, and persists the last known preview deploy state under `.minutework/deploy/preview/status.json`.
|
|
96
|
+
`deploy --preview` always revalidates and recompiles before submit, prints a local-vs-remote diff, requires confirmation unless `--yes` is passed, polls typed receipts until a terminal state, and persists the last known preview deploy state under `.minutework/deploy/preview/status.json`. This alpha is preview-first; live public delivery remains follow-on.
|
|
90
97
|
|
|
91
98
|
## Bug reporting
|
|
92
99
|
|
|
@@ -4,6 +4,9 @@ Use this workspace to build or extend MinuteWork app packs. `tenant-app` and
|
|
|
4
4
|
`sidecar` are implementation surfaces inside the pack; choose the smallest
|
|
5
5
|
surface that fits the request.
|
|
6
6
|
|
|
7
|
+
`tenant-app` is the combined web starter: public site routes at the root plus a
|
|
8
|
+
private authenticated workspace under `/app`.
|
|
9
|
+
|
|
7
10
|
## Starter Choice Matrix
|
|
8
11
|
|
|
9
12
|
The shipped product unit is an `app pack`. `tenant-app` and `sidecar` are
|
|
@@ -32,14 +35,31 @@ a concrete backend responsibility such as:
|
|
|
32
35
|
- internal API endpoint
|
|
33
36
|
- external system sync
|
|
34
37
|
|
|
38
|
+
## Public Site Defaults
|
|
39
|
+
|
|
40
|
+
- Treat `mw.core.site` as a runtime baseline capability that already exists. Do
|
|
41
|
+
not model site work as "installing" or "reconciling" the baseline.
|
|
42
|
+
- Author public content against runtime/CMS records and published-web flows, not
|
|
43
|
+
against a fixed in-repo marketing template.
|
|
44
|
+
- Use `MW_CONTENT_API_TOKEN`, `MW_PUBLIC_SITE_PROPERTY_KEY`, and
|
|
45
|
+
`MW_PUBLIC_SITE_ENV` as the standard site env contract.
|
|
46
|
+
- `MW_PUBLIC_SITE_ENV=preview` is for preview or draft-safe reads.
|
|
47
|
+
- `MW_PUBLIC_SITE_ENV=live` is for publication-safe delivery.
|
|
48
|
+
- Anonymous live delivery should prefer published snapshots instead of direct
|
|
49
|
+
runtime reads on every request.
|
|
50
|
+
- `content_structure` is open and ordered. Extend it through adjacent schemas or
|
|
51
|
+
extension packs when needed instead of bloating the baseline.
|
|
52
|
+
|
|
35
53
|
## Hard Rules
|
|
36
54
|
|
|
37
55
|
- Do not put browser auth or tenant-facing login flows in `sidecar`.
|
|
38
56
|
- Do not put heavy compute, workers, or integration orchestration in `tenant-app` route handlers.
|
|
39
57
|
- Do not choose `both` by default unless the app clearly has both UI and backend-runtime responsibilities.
|
|
40
58
|
- Public anonymous pages usually belong in `tenant-app` and ship through the hosted public-release path, not a runtime-local sidecar.
|
|
59
|
+
- Do not use `sidecar` as the default public-site renderer for pricing, docs,
|
|
60
|
+
blog, or landing pages.
|
|
41
61
|
- `sidecar` is internal-first by default.
|
|
42
62
|
|
|
43
63
|
See the files under `skills/` for deeper guidance on schema authoring,
|
|
44
|
-
app-pack structure,
|
|
45
|
-
import boundaries.
|
|
64
|
+
app-pack structure, published web, content sections, sidecar generation, event
|
|
65
|
+
flows, ontology mapping, and import boundaries.
|
|
@@ -4,3 +4,12 @@ Place focused MinuteWork architecture skills here.
|
|
|
4
4
|
|
|
5
5
|
These files are part of the canonical Builder common bundle and are allowlisted
|
|
6
6
|
for developer-local export. Skills are shared across all engine renderers.
|
|
7
|
+
|
|
8
|
+
They should describe the shipped authoring model external workspaces actually
|
|
9
|
+
receive:
|
|
10
|
+
|
|
11
|
+
- `tenant-app` is the combined public plus private web starter
|
|
12
|
+
- `mw.core.site` is a runtime baseline capability
|
|
13
|
+
- public authoring is runtime/CMS-backed
|
|
14
|
+
- anonymous live delivery should prefer published snapshots
|
|
15
|
+
- tenant-specific extensions belong in adjacent schemas or extension packs
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
An `app pack` is the shipped product unit.
|
|
4
4
|
|
|
5
5
|
- Start with declarative schema/manifests and add code surfaces only when needed.
|
|
6
|
-
- Use `tenant-app` for web
|
|
6
|
+
- Use `tenant-app` for the combined public-site plus private-app web surface.
|
|
7
7
|
- Use `sidecar` for internal APIs, workers, integrations, or Python-heavy compute.
|
|
8
|
+
- Treat `mw.core.site` as a runtime baseline capability and compose against it
|
|
9
|
+
instead of trying to install it from the workspace.
|
|
10
|
+
- Public-site authoring should stay CMS/runtime-backed, while anonymous live
|
|
11
|
+
delivery should prefer published snapshots.
|
|
8
12
|
- Keep platform-owned runtime source untouched; author only workspace-local generated source.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Content Structure And Sections
|
|
2
|
+
|
|
3
|
+
Use this skill when the request touches CMS page models, section rendering, or
|
|
4
|
+
public content composition.
|
|
5
|
+
|
|
6
|
+
- Treat `content_structure` as an open, ordered section graph, not a closed
|
|
7
|
+
fixed-template schema.
|
|
8
|
+
- Section types should stay extensible strings so tenant-specific or extension
|
|
9
|
+
pack sections can be added without rewriting the baseline.
|
|
10
|
+
- Keep ordering explicit with stable page and section sort fields where the
|
|
11
|
+
contract expects them.
|
|
12
|
+
- Map `section_type` values to UI components in the web surface instead of
|
|
13
|
+
collapsing everything into one page model.
|
|
14
|
+
- Put tenant-specific additions in adjacent schemas or extension packs instead
|
|
15
|
+
of bloating `mw.core.site`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Published Web And mw.core.site
|
|
2
|
+
|
|
3
|
+
Use this skill when the request touches public-site delivery, published-web
|
|
4
|
+
flows, or the default MinuteWork site model.
|
|
5
|
+
|
|
6
|
+
- Treat `mw.core.site` as a runtime baseline capability that already exists.
|
|
7
|
+
- `tenant-app` is the combined web starter for public routes plus the private
|
|
8
|
+
`/app` workspace.
|
|
9
|
+
- Author public content against runtime/CMS records, not a fixed in-repo site
|
|
10
|
+
starter.
|
|
11
|
+
- Use `published-web` and hosted public-release flows for anonymous delivery.
|
|
12
|
+
- `MW_CONTENT_API_TOKEN`, `MW_PUBLIC_SITE_PROPERTY_KEY`, and
|
|
13
|
+
`MW_PUBLIC_SITE_ENV` are the standard site env variables.
|
|
14
|
+
- `MW_PUBLIC_SITE_ENV=preview` is for preview or draft-safe reads.
|
|
15
|
+
- `MW_PUBLIC_SITE_ENV=live` is for publication-safe reads only.
|
|
16
|
+
- Anonymous live traffic should prefer published snapshots instead of direct
|
|
17
|
+
runtime reads on every request.
|
|
@@ -6,3 +6,7 @@ Use this skill when the request needs tenant-defined data structures.
|
|
|
6
6
|
- Prefer declarative schema changes before adding custom code surfaces.
|
|
7
7
|
- Index only the fields that need query/filter behavior.
|
|
8
8
|
- Treat runtime-backed content as the authoring source; publish safe snapshots for anonymous public delivery.
|
|
9
|
+
- Treat `mw.core.site` as already present in the runtime baseline.
|
|
10
|
+
- Keep `content_structure` open and ordered. Extend the baseline through
|
|
11
|
+
adjacent schemas or extension packs instead of hard-coding a fixed page
|
|
12
|
+
template into the schema layer.
|
|
@@ -7,3 +7,6 @@ data access.
|
|
|
7
7
|
- Prefer `secret_ref` style bindings over plaintext credentials.
|
|
8
8
|
- Treat bridge access as explicit and narrow; do not turn it into a generic data bypass.
|
|
9
9
|
- Keep private runtime payloads, traces, and raw tenant data out of public or control-plane surfaces unless a contract explicitly allows projection.
|
|
10
|
+
- Keep `MW_CONTENT_API_TOKEN` server-only.
|
|
11
|
+
- Do not leak preview-only or runtime-private site reads into anonymous live
|
|
12
|
+
delivery; published snapshots are the safe default for that boundary.
|
|
@@ -5,5 +5,9 @@ belong in `tenant-app`.
|
|
|
5
5
|
|
|
6
6
|
- Good fits: webhook handlers, workers, schedulers, ingestion, internal APIs, Python libraries.
|
|
7
7
|
- Keep browser auth and tenant-facing login flows in `tenant-app`, not here.
|
|
8
|
+
- Keep public-site rendering for pricing, docs, blog, and landing pages in
|
|
9
|
+
`tenant-app`, not here.
|
|
10
|
+
- Do not use sidecar routes as the default anonymous live delivery path when
|
|
11
|
+
published snapshots or hosted public delivery are the intended contract.
|
|
8
12
|
- Treat the sidecar as internal-first by default.
|
|
9
13
|
- Do not couple sidecar code directly to the runtime database.
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Next Tenant App Template
|
|
2
2
|
|
|
3
|
-
This directory is the canonical combined Next.js web starter for Builder.
|
|
3
|
+
This directory is the canonical combined Next.js web starter for Builder.
|
|
4
|
+
|
|
5
|
+
In the runtime Builder flow, this bundle materializes into
|
|
6
|
+
`BuilderWorkspace.sandbox_root/app/`.
|
|
7
|
+
|
|
8
|
+
In the external CLI scaffold flow, the same starter is copied into
|
|
9
|
+
`tenant-app/`.
|
|
4
10
|
|
|
5
11
|
`apps/mw-next-client` is still the repo-side seed used to harden and evolve this scaffold. It is not the runtime template location.
|
|
6
12
|
|
|
@@ -37,6 +43,9 @@ The default built-in adapter is `MinuteWorkPublicSiteContentAdapter`. It calls t
|
|
|
37
43
|
MinuteWork gateway with a server-only content token and reads the narrow
|
|
38
44
|
`mw.core.site` public-site snapshot contract.
|
|
39
45
|
|
|
46
|
+
`mw.core.site` is treated as a runtime baseline capability. This starter
|
|
47
|
+
composes against that baseline rather than trying to install it locally.
|
|
48
|
+
|
|
40
49
|
The gateway response carries an explicit boundary:
|
|
41
50
|
|
|
42
51
|
- `environment=preview` must declare `source_boundary=runtime_preview`
|
|
@@ -105,11 +114,19 @@ Use `pnpm validate` from this directory. It runs:
|
|
|
105
114
|
6. `typecheck`
|
|
106
115
|
7. `lint`
|
|
107
116
|
8. `test`
|
|
108
|
-
9. `build`
|
|
109
|
-
10. `build-storybook`
|
|
117
|
+
9. `build:validate`
|
|
118
|
+
10. `build-storybook:validate`
|
|
110
119
|
|
|
111
120
|
`template:validate` validates `template.json` against the bundled `template.schema.json`, so the template remains self-validating after Builder materializes it into a workspace sandbox.
|
|
112
121
|
|
|
122
|
+
When the CLI scaffolds this starter into `tenant-app/`, the same validation
|
|
123
|
+
contract still applies to that copied workspace surface.
|
|
124
|
+
|
|
125
|
+
The validation-only build steps inject fixture values for the required
|
|
126
|
+
production env vars and run against a local public-site fixture server, so the
|
|
127
|
+
template can prove its build contract in-repo without weakening the real
|
|
128
|
+
`pnpm build` requirement.
|
|
129
|
+
|
|
113
130
|
`template:route-contract` verifies that the combined public/private route tree, metadata routes, and content-adapter entrypoints remain present in the template.
|
|
114
131
|
|
|
115
132
|
Inside the repo, the shared schema at `runtime/builder/templates/template.schema.json` is authoritative. The bundled `template.schema.json` inside this template must stay byte-equivalent, and `template:validate` fails if the two drift.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
function resolveTurbopackRoot(startDirectory) {
|
|
8
|
+
let directory = startDirectory;
|
|
9
|
+
|
|
10
|
+
while (true) {
|
|
11
|
+
if (existsSync(path.join(directory, "pnpm-workspace.yaml"))) {
|
|
12
|
+
return directory;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parent = path.dirname(directory);
|
|
16
|
+
if (parent === directory) {
|
|
17
|
+
return startDirectory;
|
|
18
|
+
}
|
|
19
|
+
directory = parent;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nextConfig = {
|
|
24
|
+
reactStrictMode: true,
|
|
25
|
+
typedRoutes: true,
|
|
26
|
+
// Prefer the nearest workspace root so scaffolded tenant-app packages inside a
|
|
27
|
+
// monorepo and the in-repo template both resolve dependencies correctly.
|
|
28
|
+
turbopack: {
|
|
29
|
+
root: resolveTurbopackRoot(__dirname),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default nextConfig;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "next dev --hostname 127.0.0.1 --port 3000",
|
|
11
11
|
"build": "next build",
|
|
12
|
+
"build:validate": "node tools/template/with-public-site-fixture.mjs next build",
|
|
12
13
|
"start": "next start -p 3000",
|
|
13
14
|
"lint": "eslint .",
|
|
14
15
|
"test": "vitest run",
|
|
@@ -16,12 +17,13 @@
|
|
|
16
17
|
"template:validate": "node tools/template/validate-template.mjs",
|
|
17
18
|
"template:route-contract": "node tools/template/validate-route-contract.mjs",
|
|
18
19
|
"typecheck": "tsc --noEmit",
|
|
19
|
-
"validate": "pnpm template:validate && pnpm template:route-contract && pnpm typegen && pnpm design-system:tokens && pnpm design-system:check && pnpm typecheck && pnpm lint && pnpm test && pnpm build && pnpm build-storybook",
|
|
20
|
+
"validate": "pnpm template:validate && pnpm template:route-contract && pnpm typegen && pnpm design-system:tokens && pnpm design-system:check && pnpm typecheck && pnpm lint && pnpm test && pnpm build:validate && pnpm build-storybook:validate",
|
|
20
21
|
"design-system:tokens": "node tools/design-system/build-token-manifest.mjs",
|
|
21
22
|
"design-system:check": "node tools/design-system/run-checks.mjs",
|
|
22
23
|
"design-system:check:staged": "node tools/design-system/run-checks.mjs --staged",
|
|
23
24
|
"storybook": "storybook dev -p 6006",
|
|
24
25
|
"build-storybook": "storybook build",
|
|
26
|
+
"build-storybook:validate": "node tools/template/with-public-site-fixture.mjs storybook build",
|
|
25
27
|
"design-system:visual": "playwright test -c tools/design-system/playwright.config.mjs"
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { notFound } from "next/navigation";
|
|
3
|
+
|
|
4
|
+
import { ContentStructureRenderer } from "@/features/public-shell/components/section-renderer";
|
|
5
|
+
import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
|
|
6
|
+
import { getPageByPath, getSiteConfig, listPages } from "@/lib/content/adapter.server";
|
|
7
|
+
import { buildPublicMetadata } from "@/lib/public-site";
|
|
8
|
+
|
|
9
|
+
type PageParams = { path: string[] };
|
|
10
|
+
|
|
11
|
+
export async function generateStaticParams(): Promise<PageParams[]> {
|
|
12
|
+
const pages = await listPages("published");
|
|
13
|
+
return pages
|
|
14
|
+
.filter((p) => p.path !== "/")
|
|
15
|
+
.map((p) => ({
|
|
16
|
+
path: p.path.replace(/^\//, "").split("/").filter(Boolean),
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function generateMetadata({
|
|
21
|
+
params,
|
|
22
|
+
}: {
|
|
23
|
+
params: Promise<PageParams>;
|
|
24
|
+
}): Promise<Metadata> {
|
|
25
|
+
const { path: pathSegments } = await params;
|
|
26
|
+
const pagePath = `/${pathSegments.join("/")}`;
|
|
27
|
+
const page = await getPageByPath(pagePath);
|
|
28
|
+
|
|
29
|
+
if (!page) return {};
|
|
30
|
+
|
|
31
|
+
const seo = page.seo_metadata;
|
|
32
|
+
return buildPublicMetadata({
|
|
33
|
+
title: seo?.title ?? page.title,
|
|
34
|
+
description: seo?.description ?? page.title,
|
|
35
|
+
path: pagePath,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default async function CmsPage({
|
|
40
|
+
params,
|
|
41
|
+
}: {
|
|
42
|
+
params: Promise<PageParams>;
|
|
43
|
+
}) {
|
|
44
|
+
const { path: pathSegments } = await params;
|
|
45
|
+
const pagePath = `/${pathSegments.join("/")}`;
|
|
46
|
+
const [siteConfig, page] = await Promise.all([
|
|
47
|
+
getSiteConfig(),
|
|
48
|
+
getPageByPath(pagePath),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
if (!page || page.status === "draft") {
|
|
52
|
+
notFound();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sections = page.content_structure?.sections ?? [];
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<PublicSiteShell siteConfig={siteConfig} activeHref={pagePath}>
|
|
59
|
+
<div className="space-y-8">
|
|
60
|
+
<div className="space-y-3">
|
|
61
|
+
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">
|
|
62
|
+
{page.title}
|
|
63
|
+
</h1>
|
|
64
|
+
</div>
|
|
65
|
+
{sections.length > 0 ? (
|
|
66
|
+
<ContentStructureRenderer sections={sections} />
|
|
67
|
+
) : (
|
|
68
|
+
<p className="text-muted-foreground">This page has no content yet.</p>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</PublicSiteShell>
|
|
72
|
+
);
|
|
73
|
+
}
|
package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx
CHANGED
|
@@ -96,9 +96,11 @@ export function TenantDashboard({
|
|
|
96
96
|
</div>
|
|
97
97
|
<p className="text-sm leading-7 text-muted-foreground">
|
|
98
98
|
The canonical template bundle is meant to be copied into
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
a writable workspace copy before an agent edits it. In runtime
|
|
100
|
+
Builder flows that writable root lives at
|
|
101
|
+
`BuilderWorkspace.sandbox_root/app/`; in CLI scaffolds the same
|
|
102
|
+
starter lands in `tenant-app/`. This source bundle stays governed
|
|
103
|
+
and read-only in normal authoring flows.
|
|
102
104
|
</p>
|
|
103
105
|
</PanelFrame>
|
|
104
106
|
</section>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { ContentSection } from "@/lib/content/contracts";
|
|
4
|
+
|
|
5
|
+
describe("section-renderer", () => {
|
|
6
|
+
it("sorts sections by sort_order", async () => {
|
|
7
|
+
const sections: ContentSection[] = [
|
|
8
|
+
{ section_key: "b", section_type: "text", sort_order: 2, data: { heading: "B" } },
|
|
9
|
+
{ section_key: "a", section_type: "text", sort_order: 0, data: { heading: "A" } },
|
|
10
|
+
{ section_key: "c", section_type: "text", sort_order: 1, data: { heading: "C" } },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const sorted = [...sections].sort(
|
|
14
|
+
(a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
expect(sorted.map((s) => s.section_key)).toEqual(["a", "c", "b"]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("accepts arbitrary section types without error", () => {
|
|
21
|
+
const sections: ContentSection[] = [
|
|
22
|
+
{
|
|
23
|
+
section_key: "custom",
|
|
24
|
+
section_type: "my_custom_widget",
|
|
25
|
+
data: { arbitrary: "tenant-defined", nested: { deep: true } },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
section_key: "hero",
|
|
29
|
+
section_type: "hero_banner",
|
|
30
|
+
data: { heading: "Welcome" },
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Content types are open strings — no validation error.
|
|
35
|
+
expect(sections.length).toBe(2);
|
|
36
|
+
expect(sections[0]!.section_type).toBe("my_custom_widget");
|
|
37
|
+
});
|
|
38
|
+
});
|
package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { PanelFrame } from "@/design-system/patterns/panel-frame";
|
|
2
|
+
import type { ContentSection } from "@/lib/content/contracts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Registry of section_type -> React component.
|
|
6
|
+
*
|
|
7
|
+
* Section types are extensible strings. Unknown types render a
|
|
8
|
+
* generic fallback. Tenant-specific or extension-pack sections
|
|
9
|
+
* can be added here without modifying the baseline.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function HeroSection({ section }: { section: ContentSection }) {
|
|
13
|
+
const { heading, subheading, body, cta_label, cta_href } = section.data as Record<
|
|
14
|
+
string,
|
|
15
|
+
string | undefined
|
|
16
|
+
>;
|
|
17
|
+
return (
|
|
18
|
+
<PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-4 border-border/70">
|
|
19
|
+
<div className="space-y-3">
|
|
20
|
+
{subheading ? (
|
|
21
|
+
<p className="text-sm font-semibold uppercase tracking-[0.28em] text-muted-foreground">
|
|
22
|
+
{subheading}
|
|
23
|
+
</p>
|
|
24
|
+
) : null}
|
|
25
|
+
<h2 className="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
|
|
26
|
+
{heading || section.section_key}
|
|
27
|
+
</h2>
|
|
28
|
+
{body ? <p className="max-w-2xl text-base leading-8 text-muted-foreground">{body}</p> : null}
|
|
29
|
+
</div>
|
|
30
|
+
{cta_label && cta_href ? (
|
|
31
|
+
<a
|
|
32
|
+
href={cta_href}
|
|
33
|
+
className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground"
|
|
34
|
+
>
|
|
35
|
+
{cta_label}
|
|
36
|
+
</a>
|
|
37
|
+
) : null}
|
|
38
|
+
</PanelFrame>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function TextSection({ section }: { section: ContentSection }) {
|
|
43
|
+
const { heading, body, eyebrow } = section.data as Record<string, string | undefined>;
|
|
44
|
+
return (
|
|
45
|
+
<PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-3 border-border/70">
|
|
46
|
+
{eyebrow ? (
|
|
47
|
+
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
|
|
48
|
+
{eyebrow}
|
|
49
|
+
</p>
|
|
50
|
+
) : null}
|
|
51
|
+
<h3 className="text-xl font-semibold tracking-tight">{heading || section.section_key}</h3>
|
|
52
|
+
{body ? <p className="text-sm leading-7 text-muted-foreground">{body}</p> : null}
|
|
53
|
+
</PanelFrame>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function FeatureGridSection({ section }: { section: ContentSection }) {
|
|
58
|
+
const { heading, items } = section.data as {
|
|
59
|
+
heading?: string;
|
|
60
|
+
items?: { title: string; description?: string; icon?: string }[];
|
|
61
|
+
};
|
|
62
|
+
return (
|
|
63
|
+
<div className="space-y-4">
|
|
64
|
+
{heading ? <h3 className="text-xl font-semibold tracking-tight">{heading}</h3> : null}
|
|
65
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
66
|
+
{(items ?? []).map((item) => (
|
|
67
|
+
<PanelFrame
|
|
68
|
+
key={item.title}
|
|
69
|
+
tone="raised"
|
|
70
|
+
radius="xl"
|
|
71
|
+
padding="lg"
|
|
72
|
+
className="space-y-2 border-border/70"
|
|
73
|
+
>
|
|
74
|
+
<h4 className="font-semibold">{item.title}</h4>
|
|
75
|
+
{item.description ? (
|
|
76
|
+
<p className="text-sm text-muted-foreground">{item.description}</p>
|
|
77
|
+
) : null}
|
|
78
|
+
</PanelFrame>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function GenericSection({ section }: { section: ContentSection }) {
|
|
86
|
+
const { heading, body, eyebrow } = section.data as Record<string, string | undefined>;
|
|
87
|
+
return (
|
|
88
|
+
<PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-3 border-border/70">
|
|
89
|
+
{eyebrow ? (
|
|
90
|
+
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
|
|
91
|
+
{eyebrow}
|
|
92
|
+
</p>
|
|
93
|
+
) : null}
|
|
94
|
+
<h3 className="text-xl font-semibold tracking-tight">
|
|
95
|
+
{heading || section.section_type}
|
|
96
|
+
</h3>
|
|
97
|
+
{body ? <p className="text-sm leading-7 text-muted-foreground">{body}</p> : null}
|
|
98
|
+
</PanelFrame>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const SECTION_RENDERERS: Record<
|
|
103
|
+
string,
|
|
104
|
+
(props: { section: ContentSection }) => React.ReactNode
|
|
105
|
+
> = {
|
|
106
|
+
hero_banner: HeroSection,
|
|
107
|
+
hero: HeroSection,
|
|
108
|
+
text: TextSection,
|
|
109
|
+
text_block: TextSection,
|
|
110
|
+
feature_grid: FeatureGridSection,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Render a single content section by its section_type.
|
|
115
|
+
*
|
|
116
|
+
* Known types get specialized renderers. Unknown types get a generic
|
|
117
|
+
* fallback that renders heading + body from the section data.
|
|
118
|
+
*/
|
|
119
|
+
export function SectionRenderer({ section }: { section: ContentSection }) {
|
|
120
|
+
const Renderer = SECTION_RENDERERS[section.section_type] ?? GenericSection;
|
|
121
|
+
return <Renderer section={section} />;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Render an ordered list of content sections from a page's content_structure.
|
|
126
|
+
*
|
|
127
|
+
* Sections are sorted by sort_order (if present), then rendered in order.
|
|
128
|
+
*/
|
|
129
|
+
export function ContentStructureRenderer({
|
|
130
|
+
sections,
|
|
131
|
+
}: {
|
|
132
|
+
sections: ContentSection[];
|
|
133
|
+
}) {
|
|
134
|
+
const sorted = [...sections].sort(
|
|
135
|
+
(a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="grid gap-6">
|
|
140
|
+
{sorted.map((section) => (
|
|
141
|
+
<SectionRenderer key={section.section_key} section={section} />
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -302,9 +302,9 @@ describe("public content adapter", () => {
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
return publicSiteFixtureSnapshot.blog.map((entry) => ({
|
|
305
|
-
...entry,
|
|
305
|
+
...toBlogEntrySummary(entry),
|
|
306
306
|
slug: ["release", "v1"],
|
|
307
|
-
}))
|
|
307
|
+
})) as unknown as Awaited<ReturnType<PublicContentAdapter["listEntries"]>>;
|
|
308
308
|
},
|
|
309
309
|
}),
|
|
310
310
|
}));
|