minutework 0.1.32 → 0.1.33
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/assets/claude-local/skills/README.md +2 -0
- package/assets/claude-local/skills/app-pack-authoring/SKILL.md +14 -1
- package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
- package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +10 -3
- package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +9 -6
- package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +5 -4
- package/assets/templates/next-tenant-app/README.md +26 -138
- package/assets/templates/next-tenant-app/package.json +1 -0
- package/assets/templates/next-tenant-app/src/app/app/demo/page.tsx +15 -0
- package/assets/templates/next-tenant-app/src/app/app/layout.tsx +1 -4
- package/assets/templates/next-tenant-app/src/app/app/page.tsx +2 -17
- package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +9 -67
- package/assets/templates/next-tenant-app/src/app/blog/page.tsx +10 -46
- package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +9 -65
- package/assets/templates/next-tenant-app/src/app/docs/page.tsx +10 -46
- package/assets/templates/next-tenant-app/src/app/layout.tsx +8 -10
- package/assets/templates/next-tenant-app/src/app/login/page.tsx +3 -23
- package/assets/templates/next-tenant-app/src/app/page.tsx +11 -44
- package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +10 -44
- package/assets/templates/next-tenant-app/src/app/providers.tsx +2 -1
- package/assets/templates/next-tenant-app/src/app/robots.ts +7 -18
- package/assets/templates/next-tenant-app/src/app/sitemap.ts +4 -39
- package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +97 -98
- package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +43 -78
- package/assets/templates/next-tenant-app/src/features/demo/components/manifest-demo.tsx +89 -0
- package/assets/templates/next-tenant-app/src/features/public-shell/components/static-public-page.tsx +58 -0
- package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +48 -552
- package/assets/templates/next-tenant-app/src/lib/app-routes.ts +2 -2
- package/assets/templates/next-tenant-app/src/lib/public-site.ts +5 -30
- package/assets/templates/next-tenant-app/src/mw/client.ts +18 -0
- package/assets/templates/next-tenant-app/src/mw/mock.test.ts +21 -0
- package/assets/templates/next-tenant-app/src/mw/mock.ts +35 -0
- package/assets/templates/next-tenant-app/src/mw/provider.tsx +17 -0
- package/assets/templates/next-tenant-app/template.json +3 -3
- package/assets/templates/next-tenant-app/template.schema.json +1 -0
- package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +4 -5
- package/package.json +2 -2
- package/vendor/workspace-mcp/types.d.ts +4 -0
- package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +0 -89
- package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +0 -90
- package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +0 -78
- package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +0 -31
- package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +0 -16
- package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +0 -79
- package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +0 -40
- package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +0 -42
- package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +0 -29
- package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +0 -26
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +0 -40
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +0 -47
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +0 -43
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +0 -45
- package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +0 -83
- package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +0 -30
- package/assets/templates/next-tenant-app/src/app/app/page.test.ts +0 -62
- package/assets/templates/next-tenant-app/src/app/app/private-content-source.test.ts +0 -88
- package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +0 -70
- package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +0 -46
- package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +0 -70
- package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +0 -46
- package/assets/templates/next-tenant-app/src/app/login/page.test.ts +0 -55
- package/assets/templates/next-tenant-app/src/app/page.test.ts +0 -90
- package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +0 -59
- package/assets/templates/next-tenant-app/src/app/robots.test.ts +0 -40
- package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +0 -63
- package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +0 -342
- package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +0 -66
- package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +0 -108
- package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +0 -111
- package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +0 -111
- package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.test.ts +0 -38
- package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx +0 -145
- package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +0 -189
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +0 -444
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +0 -383
- package/assets/templates/next-tenant-app/src/lib/content/contracts.test.ts +0 -138
- package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +0 -399
- package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +0 -5
- package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +0 -96
- package/assets/templates/next-tenant-app/src/lib/content/release-manifest.test.ts +0 -93
- package/assets/templates/next-tenant-app/src/lib/content/release-manifest.ts +0 -123
- package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +0 -75
- package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +0 -25
- package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +0 -170
- package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +0 -661
- package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +0 -131
- package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +0 -34
- package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +0 -211
- package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +0 -151
- package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +0 -33
- package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +0 -108
|
@@ -1,399 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export const marketingPageKeys = ["home", "pricing"] as const;
|
|
4
|
-
export const publicContentKinds = ["docs", "blog"] as const;
|
|
5
|
-
export const requiredMarketingPagePaths = {
|
|
6
|
-
home: "/",
|
|
7
|
-
pricing: "/pricing",
|
|
8
|
-
} as const;
|
|
9
|
-
|
|
10
|
-
export type MarketingPageKey = (typeof marketingPageKeys)[number];
|
|
11
|
-
export type PublicContentKind = (typeof publicContentKinds)[number];
|
|
12
|
-
|
|
13
|
-
export type PublicLink = {
|
|
14
|
-
label: string;
|
|
15
|
-
href: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type ContentCollectionConfig = {
|
|
19
|
-
eyebrow: string;
|
|
20
|
-
title: string;
|
|
21
|
-
description: string;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export type SiteConfig = {
|
|
25
|
-
siteName: string;
|
|
26
|
-
siteDescription: string;
|
|
27
|
-
organizationName: string;
|
|
28
|
-
footerBlurb: string;
|
|
29
|
-
primaryCta: PublicLink;
|
|
30
|
-
secondaryCta: PublicLink;
|
|
31
|
-
primaryNavigation: PublicLink[];
|
|
32
|
-
collections: Record<PublicContentKind, ContentCollectionConfig>;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export type SeoMetadata = {
|
|
36
|
-
title: string;
|
|
37
|
-
description: string;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export type MarketingSection = {
|
|
41
|
-
eyebrow?: string;
|
|
42
|
-
title: string;
|
|
43
|
-
body: string;
|
|
44
|
-
points?: string[];
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export type MarketingPage = {
|
|
48
|
-
pageKey: MarketingPageKey;
|
|
49
|
-
path: string;
|
|
50
|
-
heroEyebrow: string;
|
|
51
|
-
heroTitle: string;
|
|
52
|
-
heroBody: string;
|
|
53
|
-
primaryCta: PublicLink;
|
|
54
|
-
secondaryCta?: PublicLink | null;
|
|
55
|
-
sections: MarketingSection[];
|
|
56
|
-
seo: SeoMetadata;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
type PublicEntrySummaryBase = {
|
|
60
|
-
title: string;
|
|
61
|
-
description: string;
|
|
62
|
-
eyebrow?: string;
|
|
63
|
-
publishedAt?: string | null;
|
|
64
|
-
readingTime?: string | null;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export type DocsEntrySummary = PublicEntrySummaryBase & {
|
|
68
|
-
kind: "docs";
|
|
69
|
-
slug: string[];
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export type BlogEntrySummary = PublicEntrySummaryBase & {
|
|
73
|
-
kind: "blog";
|
|
74
|
-
slug: [string];
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export type PublicEntrySummary = DocsEntrySummary | BlogEntrySummary;
|
|
78
|
-
|
|
79
|
-
export type PublicEntrySection = {
|
|
80
|
-
heading: string;
|
|
81
|
-
paragraphs: string[];
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
type PublicEntryBody = {
|
|
85
|
-
body: {
|
|
86
|
-
intro: string;
|
|
87
|
-
sections: PublicEntrySection[];
|
|
88
|
-
};
|
|
89
|
-
seo: SeoMetadata;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
export type DocsEntry = DocsEntrySummary & PublicEntryBody;
|
|
93
|
-
export type BlogEntry = BlogEntrySummary & PublicEntryBody;
|
|
94
|
-
export type PublicEntry = DocsEntry | BlogEntry;
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* A single section/block within a CMS page's content_structure.
|
|
98
|
-
*
|
|
99
|
-
* Section types are extensible strings — not a closed enum. This allows
|
|
100
|
-
* tenant-specific or extension-pack sections without modifying the baseline.
|
|
101
|
-
*/
|
|
102
|
-
export type ContentSection = {
|
|
103
|
-
section_key: string;
|
|
104
|
-
section_type: string;
|
|
105
|
-
sort_order?: number;
|
|
106
|
-
data: Record<string, unknown>;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* A CMS page backed by the mw.core.site runtime baseline.
|
|
111
|
-
*
|
|
112
|
-
* content_structure is an open, ordered section/block graph — not a fixed
|
|
113
|
-
* template. The section renderer maps section_type to React components.
|
|
114
|
-
*/
|
|
115
|
-
export type ContentStructurePage = {
|
|
116
|
-
page_key: string;
|
|
117
|
-
path: string;
|
|
118
|
-
title: string;
|
|
119
|
-
status: string;
|
|
120
|
-
sort_order?: number;
|
|
121
|
-
content_structure: {
|
|
122
|
-
sections: ContentSection[];
|
|
123
|
-
};
|
|
124
|
-
seo_metadata?: SeoMetadata;
|
|
125
|
-
og_metadata?: Record<string, string>;
|
|
126
|
-
visibility?: string;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
export interface PublicContentAdapter {
|
|
130
|
-
getSiteConfig(): Promise<SiteConfig>;
|
|
131
|
-
getMarketingPage(pageKey: MarketingPageKey): Promise<MarketingPage | null>;
|
|
132
|
-
listEntries(kind: PublicContentKind): Promise<PublicEntrySummary[]>;
|
|
133
|
-
getEntry(kind: PublicContentKind, slugParts: string[]): Promise<PublicEntry | null>;
|
|
134
|
-
/** Retrieve a CMS page by its URL path. Returns null if not found. */
|
|
135
|
-
getPageByPath?(path: string): Promise<ContentStructurePage | null>;
|
|
136
|
-
/** List all CMS pages, optionally filtered by status. */
|
|
137
|
-
listPages?(status?: string): Promise<ContentStructurePage[]>;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export const publicLinkSchema = z.object({
|
|
141
|
-
label: z.string().trim().min(1),
|
|
142
|
-
href: z.string().trim().min(1),
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
export const seoMetadataSchema = z.object({
|
|
146
|
-
title: z.string().trim().min(1),
|
|
147
|
-
description: z.string().trim().min(1),
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
export const contentCollectionConfigSchema = z.object({
|
|
151
|
-
eyebrow: z.string().trim().min(1),
|
|
152
|
-
title: z.string().trim().min(1),
|
|
153
|
-
description: z.string().trim().min(1),
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
export const siteConfigSchema = z.object({
|
|
157
|
-
siteName: z.string().trim().min(1),
|
|
158
|
-
siteDescription: z.string().trim().min(1),
|
|
159
|
-
organizationName: z.string().trim().min(1),
|
|
160
|
-
footerBlurb: z.string().trim().min(1),
|
|
161
|
-
primaryCta: publicLinkSchema,
|
|
162
|
-
secondaryCta: publicLinkSchema,
|
|
163
|
-
primaryNavigation: z.array(publicLinkSchema).min(1),
|
|
164
|
-
collections: z.object({
|
|
165
|
-
docs: contentCollectionConfigSchema,
|
|
166
|
-
blog: contentCollectionConfigSchema,
|
|
167
|
-
}),
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
export const marketingSectionSchema = z.object({
|
|
171
|
-
eyebrow: z.string().trim().min(1).optional(),
|
|
172
|
-
title: z.string().trim().min(1),
|
|
173
|
-
body: z.string().trim().min(1),
|
|
174
|
-
points: z.array(z.string().trim().min(1)).optional(),
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
export const marketingPageSchema = z
|
|
178
|
-
.object({
|
|
179
|
-
pageKey: z.enum(marketingPageKeys),
|
|
180
|
-
path: z.string().trim().min(1),
|
|
181
|
-
heroEyebrow: z.string().trim().min(1),
|
|
182
|
-
heroTitle: z.string().trim().min(1),
|
|
183
|
-
heroBody: z.string().trim().min(1),
|
|
184
|
-
primaryCta: publicLinkSchema,
|
|
185
|
-
secondaryCta: publicLinkSchema.nullish(),
|
|
186
|
-
sections: z.array(marketingSectionSchema).min(1),
|
|
187
|
-
seo: seoMetadataSchema,
|
|
188
|
-
})
|
|
189
|
-
.superRefine((page, context) => {
|
|
190
|
-
const requiredPath = requiredMarketingPagePaths[page.pageKey];
|
|
191
|
-
|
|
192
|
-
if (page.path !== requiredPath) {
|
|
193
|
-
context.addIssue({
|
|
194
|
-
code: z.ZodIssueCode.custom,
|
|
195
|
-
path: ["path"],
|
|
196
|
-
message: `"${page.pageKey}" marketing page must use the "${requiredPath}" route.`,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const marketingPagesSchema = z
|
|
202
|
-
.array(marketingPageSchema)
|
|
203
|
-
.superRefine((pages, context) => {
|
|
204
|
-
const counts = new Map<MarketingPageKey, number>();
|
|
205
|
-
|
|
206
|
-
for (const [index, page] of pages.entries()) {
|
|
207
|
-
const nextCount = (counts.get(page.pageKey) ?? 0) + 1;
|
|
208
|
-
counts.set(page.pageKey, nextCount);
|
|
209
|
-
|
|
210
|
-
if (nextCount > 1) {
|
|
211
|
-
context.addIssue({
|
|
212
|
-
code: z.ZodIssueCode.custom,
|
|
213
|
-
path: [index, "pageKey"],
|
|
214
|
-
message: `Duplicate marketing page for "${page.pageKey}".`,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const slugSegmentSchema = z.string().trim().min(1);
|
|
221
|
-
|
|
222
|
-
const publicEntrySummaryBaseSchema = z.object({
|
|
223
|
-
title: z.string().trim().min(1),
|
|
224
|
-
description: z.string().trim().min(1),
|
|
225
|
-
eyebrow: z.string().trim().min(1).optional(),
|
|
226
|
-
publishedAt: z.string().trim().min(1).nullable().optional(),
|
|
227
|
-
readingTime: z.string().trim().min(1).nullable().optional(),
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
export const docsEntrySummarySchema = publicEntrySummaryBaseSchema.extend({
|
|
231
|
-
kind: z.literal("docs"),
|
|
232
|
-
slug: z.array(slugSegmentSchema).min(1),
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
export const blogEntrySummarySchema = publicEntrySummaryBaseSchema.extend({
|
|
236
|
-
kind: z.literal("blog"),
|
|
237
|
-
slug: z.tuple([slugSegmentSchema]),
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
export const publicEntrySummarySchema = z.union([
|
|
241
|
-
docsEntrySummarySchema,
|
|
242
|
-
blogEntrySummarySchema,
|
|
243
|
-
]);
|
|
244
|
-
|
|
245
|
-
function normalizeSlugParts(slugParts: readonly string[]) {
|
|
246
|
-
return slugParts.map((part) => part.trim().toLowerCase()).filter(Boolean).join("/");
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const publicEntryBodySchema = z.object({
|
|
250
|
-
body: z.object({
|
|
251
|
-
intro: z.string().trim().min(1),
|
|
252
|
-
sections: z
|
|
253
|
-
.array(
|
|
254
|
-
z.object({
|
|
255
|
-
heading: z.string().trim().min(1),
|
|
256
|
-
paragraphs: z.array(z.string().trim().min(1)).min(1),
|
|
257
|
-
}),
|
|
258
|
-
)
|
|
259
|
-
.min(1),
|
|
260
|
-
}),
|
|
261
|
-
seo: seoMetadataSchema,
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
export const docsEntrySchema = docsEntrySummarySchema.extend(
|
|
265
|
-
publicEntryBodySchema.shape,
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
export const blogEntrySchema = blogEntrySummarySchema.extend(
|
|
269
|
-
publicEntryBodySchema.shape,
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
export const publicEntrySchema = z.union([docsEntrySchema, blogEntrySchema]);
|
|
273
|
-
|
|
274
|
-
export const contentSectionSchema = z.object({
|
|
275
|
-
section_key: z.string().min(1),
|
|
276
|
-
section_type: z.string().min(1),
|
|
277
|
-
sort_order: z.number().optional(),
|
|
278
|
-
data: z.record(z.unknown()),
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
export const contentStructurePageSchema = z.object({
|
|
282
|
-
page_key: z.string().min(1),
|
|
283
|
-
path: z.string().min(1),
|
|
284
|
-
title: z.string().min(1),
|
|
285
|
-
status: z.string().min(1),
|
|
286
|
-
sort_order: z.number().optional(),
|
|
287
|
-
content_structure: z.object({
|
|
288
|
-
sections: z.array(contentSectionSchema),
|
|
289
|
-
}),
|
|
290
|
-
seo_metadata: seoMetadataSchema.optional(),
|
|
291
|
-
og_metadata: z.record(z.string()).optional(),
|
|
292
|
-
visibility: z.string().optional(),
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
export const publicContentSnapshotSchema = z.object({
|
|
296
|
-
site: siteConfigSchema,
|
|
297
|
-
marketingPages: marketingPagesSchema,
|
|
298
|
-
docs: z.array(docsEntrySchema),
|
|
299
|
-
blog: z.array(blogEntrySchema),
|
|
300
|
-
pages: z.array(contentStructurePageSchema).optional(),
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
export const publicSiteSnapshotEnvironmentSchema = z.enum(["preview", "live"]);
|
|
304
|
-
export const publicSiteSnapshotSourceBoundarySchema = z.enum([
|
|
305
|
-
"runtime_preview",
|
|
306
|
-
"runtime_live",
|
|
307
|
-
"published_live",
|
|
308
|
-
]);
|
|
309
|
-
|
|
310
|
-
export const publicSiteSnapshotEnvelopeSchema = z
|
|
311
|
-
.object({
|
|
312
|
-
environment: publicSiteSnapshotEnvironmentSchema,
|
|
313
|
-
source_boundary: publicSiteSnapshotSourceBoundarySchema,
|
|
314
|
-
snapshot: publicContentSnapshotSchema,
|
|
315
|
-
})
|
|
316
|
-
.superRefine((value, context) => {
|
|
317
|
-
if (
|
|
318
|
-
value.environment === "preview" &&
|
|
319
|
-
value.source_boundary !== "runtime_preview"
|
|
320
|
-
) {
|
|
321
|
-
context.addIssue({
|
|
322
|
-
code: z.ZodIssueCode.custom,
|
|
323
|
-
path: ["source_boundary"],
|
|
324
|
-
message:
|
|
325
|
-
'Preview public-site snapshots must declare the "runtime_preview" source boundary.',
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
value.environment === "live" &&
|
|
331
|
-
!["published_live", "runtime_live"].includes(value.source_boundary)
|
|
332
|
-
) {
|
|
333
|
-
context.addIssue({
|
|
334
|
-
code: z.ZodIssueCode.custom,
|
|
335
|
-
path: ["source_boundary"],
|
|
336
|
-
message:
|
|
337
|
-
'Live public-site snapshots must declare the "published_live" or "runtime_live" source boundary.',
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
export function marketingPageResultSchema(
|
|
343
|
-
pageKey: MarketingPageKey,
|
|
344
|
-
): z.ZodType<MarketingPage | null> {
|
|
345
|
-
return marketingPageSchema.nullable().superRefine((page, context) => {
|
|
346
|
-
if (!page) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (page.pageKey !== pageKey) {
|
|
351
|
-
context.addIssue({
|
|
352
|
-
code: z.ZodIssueCode.custom,
|
|
353
|
-
path: ["pageKey"],
|
|
354
|
-
message: `Expected "${pageKey}" marketing page.`,
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
export function publicEntrySummaryListSchema(
|
|
361
|
-
kind: PublicContentKind,
|
|
362
|
-
): z.ZodType<PublicEntrySummary[]> {
|
|
363
|
-
return z.array(kind === "docs" ? docsEntrySummarySchema : blogEntrySummarySchema);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
export function publicEntryResultSchema(
|
|
367
|
-
kind: PublicContentKind,
|
|
368
|
-
slugParts: readonly string[],
|
|
369
|
-
): z.ZodType<PublicEntry | null> {
|
|
370
|
-
const refineEntryResult = <
|
|
371
|
-
TEntry extends DocsEntry | BlogEntry | null,
|
|
372
|
-
>(
|
|
373
|
-
entry: TEntry,
|
|
374
|
-
context: z.RefinementCtx,
|
|
375
|
-
) => {
|
|
376
|
-
if (!entry) {
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (normalizeSlugParts(entry.slug) !== normalizeSlugParts(slugParts)) {
|
|
381
|
-
context.addIssue({
|
|
382
|
-
code: z.ZodIssueCode.custom,
|
|
383
|
-
path: ["slug"],
|
|
384
|
-
message: `Expected "${kind}" entry slug to match "${slugParts.join("/")}".`,
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
if (kind === "docs") {
|
|
390
|
-
return docsEntrySchema.nullable().superRefine(refineEntryResult);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return blogEntrySchema.nullable().superRefine(refineEntryResult);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
export type PublicContentSnapshot = z.infer<typeof publicContentSnapshotSchema>;
|
|
397
|
-
export type PublicSiteSnapshotEnvelope = z.infer<
|
|
398
|
-
typeof publicSiteSnapshotEnvelopeSchema
|
|
399
|
-
>;
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
MarketingPage,
|
|
3
|
-
MarketingPageKey,
|
|
4
|
-
SiteConfig,
|
|
5
|
-
} from "@/lib/content/contracts";
|
|
6
|
-
|
|
7
|
-
const emptyMarketingPageConfig: Record<
|
|
8
|
-
MarketingPageKey,
|
|
9
|
-
Pick<
|
|
10
|
-
MarketingPage,
|
|
11
|
-
| "heroEyebrow"
|
|
12
|
-
| "heroTitle"
|
|
13
|
-
| "heroBody"
|
|
14
|
-
| "path"
|
|
15
|
-
| "sections"
|
|
16
|
-
>
|
|
17
|
-
> = {
|
|
18
|
-
home: {
|
|
19
|
-
heroEyebrow: "Public site",
|
|
20
|
-
heroTitle: "Your public site is ready for published content.",
|
|
21
|
-
heroBody:
|
|
22
|
-
"Connect this starter to MinuteWork public-site content and publish your first marketing page, docs entry, or blog post.",
|
|
23
|
-
path: "/",
|
|
24
|
-
sections: [
|
|
25
|
-
{
|
|
26
|
-
eyebrow: "First run",
|
|
27
|
-
title: "No published home page yet",
|
|
28
|
-
body:
|
|
29
|
-
"This starter now reads public-site content from the MinuteWork gateway during development and build.",
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
eyebrow: "Preview",
|
|
33
|
-
title: "Use preview for local authoring",
|
|
34
|
-
body:
|
|
35
|
-
"Set MW_PUBLIC_SITE_ENV=preview to render draft-safe preview content through the same API contract.",
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
eyebrow: "Publish",
|
|
39
|
-
title: "Live stays publication-safe",
|
|
40
|
-
body:
|
|
41
|
-
"Switch builds and deploys to MW_PUBLIC_SITE_ENV=live once the property has published content available.",
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
pricing: {
|
|
46
|
-
heroEyebrow: "Pricing",
|
|
47
|
-
heroTitle: "Add your first published pricing page.",
|
|
48
|
-
heroBody:
|
|
49
|
-
"The pricing route is live, but the property has not published pricing content yet. Publish a pricing page to replace this empty-state shell.",
|
|
50
|
-
path: "/pricing",
|
|
51
|
-
sections: [
|
|
52
|
-
{
|
|
53
|
-
eyebrow: "Authoring source",
|
|
54
|
-
title: "Published pages stay externalized",
|
|
55
|
-
body:
|
|
56
|
-
"Marketing content now comes from the MinuteWork public-site snapshot API instead of seeded in-repo copy.",
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
eyebrow: "Build behavior",
|
|
60
|
-
title: "Static-first output still works",
|
|
61
|
-
body:
|
|
62
|
-
"Builds succeed with valid empty snapshots, so fresh properties can ship before authored content exists.",
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
eyebrow: "Next step",
|
|
66
|
-
title: "Publish pricing content when ready",
|
|
67
|
-
body:
|
|
68
|
-
"As soon as the property publishes a pricing page, this shell is replaced automatically by the fetched snapshot.",
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export function buildEmptyMarketingPage(
|
|
75
|
-
pageKey: MarketingPageKey,
|
|
76
|
-
siteConfig: SiteConfig,
|
|
77
|
-
): MarketingPage {
|
|
78
|
-
const config = emptyMarketingPageConfig[pageKey];
|
|
79
|
-
const titlePrefix =
|
|
80
|
-
pageKey === "home" ? siteConfig.siteName : `${siteConfig.siteName} Pricing`;
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
pageKey,
|
|
84
|
-
path: config.path,
|
|
85
|
-
heroEyebrow: config.heroEyebrow,
|
|
86
|
-
heroTitle: config.heroTitle,
|
|
87
|
-
heroBody: config.heroBody,
|
|
88
|
-
primaryCta: siteConfig.primaryCta,
|
|
89
|
-
secondaryCta: siteConfig.secondaryCta,
|
|
90
|
-
sections: config.sections,
|
|
91
|
-
seo: {
|
|
92
|
-
title: titlePrefix,
|
|
93
|
-
description: siteConfig.siteDescription,
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { buildReleaseManifest, releaseManifestSchema } from "./release-manifest";
|
|
4
|
-
|
|
5
|
-
describe("release-manifest", () => {
|
|
6
|
-
it("builds a valid manifest from composition input", () => {
|
|
7
|
-
const manifest = buildReleaseManifest({
|
|
8
|
-
appPackId: "mw.core.site",
|
|
9
|
-
snapshotSchemaVersion: 1,
|
|
10
|
-
releaseClass: "static_export",
|
|
11
|
-
config: { site_title: "Test Site" },
|
|
12
|
-
pages: [
|
|
13
|
-
{
|
|
14
|
-
page_key: "home",
|
|
15
|
-
path: "/",
|
|
16
|
-
title: "Home",
|
|
17
|
-
sections: [{ section_key: "hero", section_type: "hero", data: {} }],
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
page_key: "about",
|
|
21
|
-
path: "/about",
|
|
22
|
-
title: "About",
|
|
23
|
-
sections: [],
|
|
24
|
-
},
|
|
25
|
-
],
|
|
26
|
-
navigation: [
|
|
27
|
-
{ nav_key: "header", slot: "header", items: [{ label: "Home" }] },
|
|
28
|
-
],
|
|
29
|
-
forms: [
|
|
30
|
-
{ form_key: "contact", schema: { fields: [{ name: "email" }] } },
|
|
31
|
-
],
|
|
32
|
-
contentDigest: "sha256-abc123",
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const parsed = releaseManifestSchema.safeParse(manifest);
|
|
36
|
-
expect(parsed.success).toBe(true);
|
|
37
|
-
expect(manifest.manifest_version).toBe(1);
|
|
38
|
-
expect(manifest.composition.config_updated).toBe(true);
|
|
39
|
-
expect(manifest.composition.pages).toHaveLength(2);
|
|
40
|
-
expect(manifest.composition.pages[0]!.section_count).toBe(1);
|
|
41
|
-
expect(manifest.composition.pages[1]!.section_count).toBe(0);
|
|
42
|
-
expect(manifest.composition.navigation[0]!.item_count).toBe(1);
|
|
43
|
-
expect(manifest.composition.forms[0]!.field_count).toBe(1);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("handles null config", () => {
|
|
47
|
-
const manifest = buildReleaseManifest({
|
|
48
|
-
appPackId: "mw.core.site",
|
|
49
|
-
snapshotSchemaVersion: 1,
|
|
50
|
-
releaseClass: "ssr_container",
|
|
51
|
-
config: null,
|
|
52
|
-
pages: [],
|
|
53
|
-
navigation: [],
|
|
54
|
-
forms: [],
|
|
55
|
-
contentDigest: "sha256-empty",
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
expect(manifest.composition.config_updated).toBe(false);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("supports attached external site metadata without adding a new release class", () => {
|
|
62
|
-
const manifest = buildReleaseManifest({
|
|
63
|
-
appPackId: "mw.core.site",
|
|
64
|
-
snapshotSchemaVersion: 1,
|
|
65
|
-
releaseClass: "runtime_local_sidecar",
|
|
66
|
-
config: null,
|
|
67
|
-
pages: [],
|
|
68
|
-
navigation: [],
|
|
69
|
-
forms: [],
|
|
70
|
-
attachedSite: {
|
|
71
|
-
mode: "attached_app",
|
|
72
|
-
adapter_module: "src/lib/content/custom-adapter.ts",
|
|
73
|
-
renderer: "external_next",
|
|
74
|
-
publish_target: "repo-sync",
|
|
75
|
-
},
|
|
76
|
-
contentDigest: "sha256-attached",
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const parsed = releaseManifestSchema.safeParse(manifest);
|
|
80
|
-
expect(parsed.success).toBe(true);
|
|
81
|
-
expect(manifest.attached_site?.mode).toBe("attached_app");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("validates against the schema", () => {
|
|
85
|
-
const invalid = {
|
|
86
|
-
manifest_version: 2, // wrong version
|
|
87
|
-
app_pack_id: "",
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const parsed = releaseManifestSchema.safeParse(invalid);
|
|
91
|
-
expect(parsed.success).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
});
|