minutework 0.1.32 → 0.1.34
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 +24 -1
- package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
- package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +19 -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 +37 -135
- 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,383 +0,0 @@
|
|
|
1
|
-
import "server-only";
|
|
2
|
-
|
|
3
|
-
import { cache } from "react";
|
|
4
|
-
import { z, type ZodType } from "zod";
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
ContentStructurePage,
|
|
8
|
-
MarketingPage,
|
|
9
|
-
MarketingPageKey,
|
|
10
|
-
PublicContentAdapter,
|
|
11
|
-
PublicContentKind,
|
|
12
|
-
PublicContentSnapshot,
|
|
13
|
-
PublicEntry,
|
|
14
|
-
PublicEntrySummary,
|
|
15
|
-
PublicSiteSnapshotEnvelope,
|
|
16
|
-
SiteConfig,
|
|
17
|
-
} from "@/lib/content/contracts";
|
|
18
|
-
import {
|
|
19
|
-
contentStructurePageSchema,
|
|
20
|
-
marketingPageResultSchema,
|
|
21
|
-
publicEntryResultSchema,
|
|
22
|
-
publicEntrySummaryListSchema,
|
|
23
|
-
publicContentSnapshotSchema,
|
|
24
|
-
publicSiteSnapshotEnvelopeSchema,
|
|
25
|
-
siteConfigSchema,
|
|
26
|
-
} from "@/lib/content/contracts";
|
|
27
|
-
import { resolveCustomPublicContentAdapter } from "@/lib/content/custom-adapter";
|
|
28
|
-
import { env } from "@/lib/platform/env.server";
|
|
29
|
-
|
|
30
|
-
export const PUBLIC_CONTENT_REVALIDATE_SECONDS = 300;
|
|
31
|
-
|
|
32
|
-
function normalizeSlugParts(slugParts: readonly string[]) {
|
|
33
|
-
return slugParts.map((part) => part.trim().toLowerCase()).filter(Boolean).join("/");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function compareSlugParts(left: readonly string[], right: readonly string[]) {
|
|
37
|
-
return normalizeSlugParts(left) === normalizeSlugParts(right);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function sortEntriesByDate(entries: readonly PublicEntrySummary[]) {
|
|
41
|
-
return [...entries].sort((left, right) => {
|
|
42
|
-
const leftTime = left.publishedAt ? Date.parse(left.publishedAt) : 0;
|
|
43
|
-
const rightTime = right.publishedAt ? Date.parse(right.publishedAt) : 0;
|
|
44
|
-
return rightTime - leftTime;
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function formatZodIssues(
|
|
49
|
-
issues: readonly { message: string; path: readonly (string | number)[] }[],
|
|
50
|
-
) {
|
|
51
|
-
return issues
|
|
52
|
-
.map((issue) => {
|
|
53
|
-
const path = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
54
|
-
return `${path}: ${issue.message}`;
|
|
55
|
-
})
|
|
56
|
-
.join("; ");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function validateAdapterResult<T>(
|
|
60
|
-
adapterLabel: string,
|
|
61
|
-
methodLabel: string,
|
|
62
|
-
schema: ZodType<T>,
|
|
63
|
-
value: unknown,
|
|
64
|
-
) {
|
|
65
|
-
const parsedValue = schema.safeParse(value);
|
|
66
|
-
|
|
67
|
-
if (!parsedValue.success) {
|
|
68
|
-
throw new Error(
|
|
69
|
-
`${adapterLabel} returned invalid ${methodLabel}: ${formatZodIssues(parsedValue.error.issues)}`,
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return parsedValue.data;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function requirePublicContentEnv(key: string, value: string | undefined) {
|
|
77
|
-
if (!value) {
|
|
78
|
-
throw new Error(`${key} is required for MW_PUBLIC_CONTENT_SOURCE=minutework_cms.`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return value;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export class ValidatedPublicContentAdapter implements PublicContentAdapter {
|
|
85
|
-
constructor(
|
|
86
|
-
private readonly adapter: PublicContentAdapter,
|
|
87
|
-
private readonly adapterLabel: string,
|
|
88
|
-
) {}
|
|
89
|
-
|
|
90
|
-
async getSiteConfig(): Promise<SiteConfig> {
|
|
91
|
-
return validateAdapterResult(
|
|
92
|
-
this.adapterLabel,
|
|
93
|
-
"site config",
|
|
94
|
-
siteConfigSchema,
|
|
95
|
-
await this.adapter.getSiteConfig(),
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async getMarketingPage(pageKey: MarketingPageKey): Promise<MarketingPage | null> {
|
|
100
|
-
return validateAdapterResult(
|
|
101
|
-
this.adapterLabel,
|
|
102
|
-
`marketing page for "${pageKey}"`,
|
|
103
|
-
marketingPageResultSchema(pageKey),
|
|
104
|
-
await this.adapter.getMarketingPage(pageKey),
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async listEntries(kind: PublicContentKind): Promise<PublicEntrySummary[]> {
|
|
109
|
-
return validateAdapterResult(
|
|
110
|
-
this.adapterLabel,
|
|
111
|
-
`${kind} entry summaries`,
|
|
112
|
-
publicEntrySummaryListSchema(kind),
|
|
113
|
-
await this.adapter.listEntries(kind),
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async getEntry(kind: PublicContentKind, slugParts: string[]): Promise<PublicEntry | null> {
|
|
118
|
-
return validateAdapterResult(
|
|
119
|
-
this.adapterLabel,
|
|
120
|
-
`${kind} entry for "${normalizeSlugParts(slugParts)}"`,
|
|
121
|
-
publicEntryResultSchema(kind, slugParts),
|
|
122
|
-
await this.adapter.getEntry(kind, slugParts),
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async getPageByPath(path: string): Promise<ContentStructurePage | null> {
|
|
127
|
-
if (!this.adapter.getPageByPath) return null;
|
|
128
|
-
return validateAdapterResult(
|
|
129
|
-
this.adapterLabel,
|
|
130
|
-
`CMS page for path "${path}"`,
|
|
131
|
-
contentStructurePageSchema.nullable(),
|
|
132
|
-
await this.adapter.getPageByPath(path),
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async listPages(status?: string): Promise<ContentStructurePage[]> {
|
|
137
|
-
if (!this.adapter.listPages) return [];
|
|
138
|
-
return validateAdapterResult(
|
|
139
|
-
this.adapterLabel,
|
|
140
|
-
"CMS page list",
|
|
141
|
-
z.array(contentStructurePageSchema),
|
|
142
|
-
await this.adapter.listPages(status),
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export class PublicSiteSnapshotContentAdapter implements PublicContentAdapter {
|
|
148
|
-
constructor(private readonly loadSnapshot: () => Promise<PublicContentSnapshot>) {}
|
|
149
|
-
|
|
150
|
-
async getSiteConfig(): Promise<SiteConfig> {
|
|
151
|
-
return (await this.loadSnapshot()).site;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async getMarketingPage(pageKey: MarketingPageKey): Promise<MarketingPage | null> {
|
|
155
|
-
return (
|
|
156
|
-
(await this.loadSnapshot()).marketingPages.find((page) => page.pageKey === pageKey) ??
|
|
157
|
-
null
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async listEntries(kind: PublicContentKind): Promise<PublicEntrySummary[]> {
|
|
162
|
-
return sortEntriesByDate((await this.loadSnapshot())[kind]);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async getEntry(kind: PublicContentKind, slugParts: string[]): Promise<PublicEntry | null> {
|
|
166
|
-
return (
|
|
167
|
-
(await this.loadSnapshot())[kind].find((entry) =>
|
|
168
|
-
compareSlugParts(entry.slug, slugParts),
|
|
169
|
-
) ?? null
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async getPageByPath(path: string): Promise<ContentStructurePage | null> {
|
|
174
|
-
const snapshot = await this.loadSnapshot();
|
|
175
|
-
const pages = snapshot.pages ?? [];
|
|
176
|
-
const normalizedPath = path.replace(/\/+$/, "") || "/";
|
|
177
|
-
return pages.find((p) => p.path === normalizedPath) ?? null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async listPages(status?: string): Promise<ContentStructurePage[]> {
|
|
181
|
-
const snapshot = await this.loadSnapshot();
|
|
182
|
-
const pages = snapshot.pages ?? [];
|
|
183
|
-
if (status) {
|
|
184
|
-
return pages.filter((p) => p.status === status);
|
|
185
|
-
}
|
|
186
|
-
return pages;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export async function fetchPublicSiteSnapshotEnvelope(input: {
|
|
191
|
-
contentApiToken: string;
|
|
192
|
-
environment: "preview" | "live";
|
|
193
|
-
platformBaseUrl: string;
|
|
194
|
-
propertyKey: string;
|
|
195
|
-
}): Promise<PublicSiteSnapshotEnvelope> {
|
|
196
|
-
const requestUrl = new URL(
|
|
197
|
-
`/api/v1/developer/public-site/snapshots/${encodeURIComponent(input.propertyKey)}/`,
|
|
198
|
-
`${input.platformBaseUrl.replace(/\/$/, "")}/`,
|
|
199
|
-
);
|
|
200
|
-
requestUrl.searchParams.set("environment", input.environment);
|
|
201
|
-
|
|
202
|
-
const response = await fetch(requestUrl.toString(), {
|
|
203
|
-
headers: {
|
|
204
|
-
accept: "application/json",
|
|
205
|
-
authorization: `Bearer ${input.contentApiToken}`,
|
|
206
|
-
},
|
|
207
|
-
next: {
|
|
208
|
-
revalidate: PUBLIC_CONTENT_REVALIDATE_SECONDS,
|
|
209
|
-
tags: [
|
|
210
|
-
"public-site-content",
|
|
211
|
-
`public-site-content:${input.propertyKey}`,
|
|
212
|
-
`public-site-content:${input.environment}`,
|
|
213
|
-
],
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
if (!response.ok) {
|
|
218
|
-
throw new Error(
|
|
219
|
-
`Unable to load public-site ${input.environment} snapshot for "${input.propertyKey}" (${response.status}).`,
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const payload = await response.json();
|
|
224
|
-
const parsedEnvelope = publicSiteSnapshotEnvelopeSchema.safeParse(payload);
|
|
225
|
-
|
|
226
|
-
if (!parsedEnvelope.success) {
|
|
227
|
-
throw new Error(
|
|
228
|
-
`Public-site snapshot envelope is invalid: ${formatZodIssues(parsedEnvelope.error.issues)}`,
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return parsedEnvelope.data;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const loadPublicSiteSnapshotEnvelope = cache(async () =>
|
|
236
|
-
fetchPublicSiteSnapshotEnvelope({
|
|
237
|
-
contentApiToken: requirePublicContentEnv(
|
|
238
|
-
"MW_CONTENT_API_TOKEN",
|
|
239
|
-
env.MW_CONTENT_API_TOKEN,
|
|
240
|
-
),
|
|
241
|
-
environment: env.MW_PUBLIC_SITE_ENV,
|
|
242
|
-
platformBaseUrl: env.MW_PLATFORM_BASE_URL,
|
|
243
|
-
propertyKey: requirePublicContentEnv(
|
|
244
|
-
"MW_PUBLIC_SITE_PROPERTY_KEY",
|
|
245
|
-
env.MW_PUBLIC_SITE_PROPERTY_KEY,
|
|
246
|
-
),
|
|
247
|
-
}),
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
export class MinuteWorkPublicSiteContentAdapter extends PublicSiteSnapshotContentAdapter {
|
|
251
|
-
constructor(
|
|
252
|
-
loadEnvelope: () => Promise<PublicSiteSnapshotEnvelope> = loadPublicSiteSnapshotEnvelope,
|
|
253
|
-
) {
|
|
254
|
-
super(async () => (await loadEnvelope()).snapshot);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const loadStaticPublicContentSnapshot = cache(async () => {
|
|
259
|
-
const [{ readFile }, { resolve }] = await Promise.all([
|
|
260
|
-
import("node:fs/promises"),
|
|
261
|
-
import("node:path"),
|
|
262
|
-
]);
|
|
263
|
-
const contentPath = resolve(
|
|
264
|
-
/* turbopackIgnore: true */ process.cwd(),
|
|
265
|
-
env.MW_STATIC_PUBLIC_CONTENT_PATH,
|
|
266
|
-
);
|
|
267
|
-
let payload: unknown;
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
payload = JSON.parse(await readFile(contentPath, "utf8"));
|
|
271
|
-
} catch (error) {
|
|
272
|
-
throw new Error(
|
|
273
|
-
`Unable to load static public content from "${env.MW_STATIC_PUBLIC_CONTENT_PATH}": ${error instanceof Error ? error.message : String(error)}`,
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const parsedSnapshot = publicContentSnapshotSchema.safeParse(payload);
|
|
278
|
-
|
|
279
|
-
if (!parsedSnapshot.success) {
|
|
280
|
-
throw new Error(
|
|
281
|
-
`Static public content is invalid: ${formatZodIssues(parsedSnapshot.error.issues)}`,
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return parsedSnapshot.data;
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
export class StaticJsonPublicContentAdapter extends PublicSiteSnapshotContentAdapter {
|
|
289
|
-
constructor(
|
|
290
|
-
loadSnapshot: () => Promise<PublicContentSnapshot> = loadStaticPublicContentSnapshot,
|
|
291
|
-
) {
|
|
292
|
-
super(loadSnapshot);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
class DisabledPublicContentAdapter implements PublicContentAdapter {
|
|
297
|
-
private throwDisabled(): never {
|
|
298
|
-
throw new Error("Public content is disabled by MW_PUBLIC_CONTENT_SOURCE=none.");
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
async getSiteConfig(): Promise<SiteConfig> {
|
|
302
|
-
this.throwDisabled();
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async getMarketingPage(): Promise<MarketingPage | null> {
|
|
306
|
-
this.throwDisabled();
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async listEntries(): Promise<PublicEntrySummary[]> {
|
|
310
|
-
this.throwDisabled();
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async getEntry(): Promise<PublicEntry | null> {
|
|
314
|
-
this.throwDisabled();
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async getPageByPath(): Promise<ContentStructurePage | null> {
|
|
318
|
-
this.throwDisabled();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async listPages(): Promise<ContentStructurePage[]> {
|
|
322
|
-
this.throwDisabled();
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const defaultPublicContentAdapter = new ValidatedPublicContentAdapter(
|
|
327
|
-
new MinuteWorkPublicSiteContentAdapter(),
|
|
328
|
-
"MinuteWork public-site content adapter",
|
|
329
|
-
);
|
|
330
|
-
const staticJsonPublicContentAdapter = new ValidatedPublicContentAdapter(
|
|
331
|
-
new StaticJsonPublicContentAdapter(),
|
|
332
|
-
"Static JSON public content adapter",
|
|
333
|
-
);
|
|
334
|
-
const disabledPublicContentAdapter = new DisabledPublicContentAdapter();
|
|
335
|
-
|
|
336
|
-
export const getPublicContentAdapter = cache(() => {
|
|
337
|
-
if (env.MW_PUBLIC_CONTENT_SOURCE === "custom") {
|
|
338
|
-
const customAdapter = resolveCustomPublicContentAdapter();
|
|
339
|
-
|
|
340
|
-
if (!customAdapter) {
|
|
341
|
-
throw new Error(
|
|
342
|
-
"MW_PUBLIC_CONTENT_SOURCE=custom requires resolveCustomPublicContentAdapter() to return a PublicContentAdapter.",
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return new ValidatedPublicContentAdapter(
|
|
347
|
-
customAdapter,
|
|
348
|
-
"Custom public content adapter",
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (env.MW_PUBLIC_CONTENT_SOURCE === "static_json") {
|
|
353
|
-
return staticJsonPublicContentAdapter;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (env.MW_PUBLIC_CONTENT_SOURCE === "none") {
|
|
357
|
-
return disabledPublicContentAdapter;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return defaultPublicContentAdapter;
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
export const getSiteConfig = cache(async () => getPublicContentAdapter().getSiteConfig());
|
|
364
|
-
|
|
365
|
-
export const getMarketingPage = cache(async (pageKey: MarketingPageKey) =>
|
|
366
|
-
getPublicContentAdapter().getMarketingPage(pageKey),
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
export const listEntries = cache(async (kind: PublicContentKind) =>
|
|
370
|
-
getPublicContentAdapter().listEntries(kind),
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
export const getEntry = cache(async (kind: PublicContentKind, slugParts: string[]) =>
|
|
374
|
-
getPublicContentAdapter().getEntry(kind, slugParts),
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
export const getPageByPath = cache(async (path: string) =>
|
|
378
|
-
getPublicContentAdapter().getPageByPath(path),
|
|
379
|
-
);
|
|
380
|
-
|
|
381
|
-
export const listPages = cache(async (status?: string) =>
|
|
382
|
-
getPublicContentAdapter().listPages(status),
|
|
383
|
-
);
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
contentSectionSchema,
|
|
5
|
-
contentStructurePageSchema,
|
|
6
|
-
publicContentSnapshotSchema,
|
|
7
|
-
} from "./contracts";
|
|
8
|
-
|
|
9
|
-
describe("content_structure contracts", () => {
|
|
10
|
-
it("validates a page with arbitrary section types", () => {
|
|
11
|
-
const page = {
|
|
12
|
-
page_key: "landing",
|
|
13
|
-
path: "/landing",
|
|
14
|
-
title: "Landing Page",
|
|
15
|
-
status: "published",
|
|
16
|
-
sort_order: 0,
|
|
17
|
-
content_structure: {
|
|
18
|
-
sections: [
|
|
19
|
-
{
|
|
20
|
-
section_key: "hero",
|
|
21
|
-
section_type: "hero_banner",
|
|
22
|
-
sort_order: 0,
|
|
23
|
-
data: { heading: "Welcome", subheading: "To our site" },
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
section_key: "features",
|
|
27
|
-
section_type: "custom_feature_grid",
|
|
28
|
-
sort_order: 1,
|
|
29
|
-
data: {
|
|
30
|
-
columns: 3,
|
|
31
|
-
items: [
|
|
32
|
-
{ title: "Fast", icon: "bolt" },
|
|
33
|
-
{ title: "Secure", icon: "lock" },
|
|
34
|
-
],
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
section_key: "tenant_custom",
|
|
39
|
-
section_type: "my_custom_widget",
|
|
40
|
-
sort_order: 2,
|
|
41
|
-
data: { arbitrary: "tenant-defined", nested: { deep: true } },
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
seo_metadata: {
|
|
46
|
-
title: "Landing Page",
|
|
47
|
-
description: "A landing page with arbitrary sections",
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const result = contentStructurePageSchema.safeParse(page);
|
|
52
|
-
expect(result.success).toBe(true);
|
|
53
|
-
if (result.success) {
|
|
54
|
-
expect(result.data.content_structure.sections).toHaveLength(3);
|
|
55
|
-
expect(result.data.content_structure.sections[2]!.section_type).toBe(
|
|
56
|
-
"my_custom_widget",
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("rejects a section without section_key", () => {
|
|
62
|
-
const section = {
|
|
63
|
-
section_type: "text",
|
|
64
|
-
data: { heading: "Heading" },
|
|
65
|
-
};
|
|
66
|
-
const result = contentSectionSchema.safeParse(section);
|
|
67
|
-
expect(result.success).toBe(false);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("allows empty sections array", () => {
|
|
71
|
-
const page = {
|
|
72
|
-
page_key: "empty",
|
|
73
|
-
path: "/empty",
|
|
74
|
-
title: "Empty Page",
|
|
75
|
-
status: "draft",
|
|
76
|
-
content_structure: { sections: [] },
|
|
77
|
-
};
|
|
78
|
-
const result = contentStructurePageSchema.safeParse(page);
|
|
79
|
-
expect(result.success).toBe(true);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("snapshot schema accepts optional pages array", () => {
|
|
83
|
-
const snapshot = {
|
|
84
|
-
site: {
|
|
85
|
-
siteName: "Test",
|
|
86
|
-
siteDescription: "Test site",
|
|
87
|
-
organizationName: "Org",
|
|
88
|
-
footerBlurb: "Footer",
|
|
89
|
-
primaryCta: { label: "CTA", href: "/" },
|
|
90
|
-
secondaryCta: { label: "CTA2", href: "/docs" },
|
|
91
|
-
primaryNavigation: [{ label: "Home", href: "/" }],
|
|
92
|
-
collections: {
|
|
93
|
-
docs: { eyebrow: "Docs", title: "Docs", description: "Documentation" },
|
|
94
|
-
blog: { eyebrow: "Blog", title: "Blog", description: "Updates" },
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
marketingPages: [],
|
|
98
|
-
docs: [],
|
|
99
|
-
blog: [],
|
|
100
|
-
pages: [
|
|
101
|
-
{
|
|
102
|
-
page_key: "about",
|
|
103
|
-
path: "/about",
|
|
104
|
-
title: "About",
|
|
105
|
-
status: "published",
|
|
106
|
-
content_structure: { sections: [] },
|
|
107
|
-
},
|
|
108
|
-
],
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const result = publicContentSnapshotSchema.safeParse(snapshot);
|
|
112
|
-
expect(result.success).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("snapshot schema works without pages field (backward compat)", () => {
|
|
116
|
-
const snapshot = {
|
|
117
|
-
site: {
|
|
118
|
-
siteName: "Test",
|
|
119
|
-
siteDescription: "Test site",
|
|
120
|
-
organizationName: "Org",
|
|
121
|
-
footerBlurb: "Footer",
|
|
122
|
-
primaryCta: { label: "CTA", href: "/" },
|
|
123
|
-
secondaryCta: { label: "CTA2", href: "/docs" },
|
|
124
|
-
primaryNavigation: [{ label: "Home", href: "/" }],
|
|
125
|
-
collections: {
|
|
126
|
-
docs: { eyebrow: "Docs", title: "Docs", description: "Documentation" },
|
|
127
|
-
blog: { eyebrow: "Blog", title: "Blog", description: "Updates" },
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
marketingPages: [],
|
|
131
|
-
docs: [],
|
|
132
|
-
blog: [],
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const result = publicContentSnapshotSchema.safeParse(snapshot);
|
|
136
|
-
expect(result.success).toBe(true);
|
|
137
|
-
});
|
|
138
|
-
});
|