minutework 0.1.25 → 0.1.27
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/CLAUDE.md.template +4 -2
- package/assets/claude-local/skills/project-overview-and-strategy/SKILL.md +5 -3
- package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +3 -1
- package/assets/templates/next-tenant-app/.env.example +4 -3
- package/assets/templates/next-tenant-app/README.md +9 -4
- package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +17 -1
- package/assets/templates/next-tenant-app/src/app/app/private-content-source.test.ts +88 -0
- package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +17 -1
- package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +4 -0
- package/assets/templates/next-tenant-app/src/app/blog/page.tsx +15 -1
- package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +17 -1
- package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +4 -0
- package/assets/templates/next-tenant-app/src/app/docs/page.tsx +15 -1
- package/assets/templates/next-tenant-app/src/app/layout.tsx +4 -2
- package/assets/templates/next-tenant-app/src/app/page.test.ts +4 -0
- package/assets/templates/next-tenant-app/src/app/page.tsx +15 -1
- package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +4 -0
- package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +15 -1
- package/assets/templates/next-tenant-app/src/app/robots.test.ts +24 -4
- package/assets/templates/next-tenant-app/src/app/robots.ts +15 -1
- package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +16 -2
- package/assets/templates/next-tenant-app/src/app/sitemap.ts +21 -7
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +102 -0
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +140 -35
- package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +103 -0
- package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +84 -20
- package/assets/templates/next-tenant-app/src/lib/public-site.test.ts +1 -1
- package/assets/templates/next-tenant-app/src/lib/public-site.ts +30 -6
- package/assets/templates/next-tenant-app/tools/template/with-public-site-fixture.mjs +1 -0
- package/assets/templates/next-tenant-app/vitest.config.ts +2 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import "server-only";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
const DEFAULT_LOCAL_PLATFORM_BASE_URL = "http://127.0.0.1:8000";
|
|
6
|
+
const DEFAULT_STATIC_PUBLIC_CONTENT_PATH = "content/public-site.json";
|
|
6
7
|
|
|
7
8
|
const exampleFlagSchema = z.preprocess(
|
|
8
9
|
(value) => value ?? "false",
|
|
@@ -18,35 +19,96 @@ const requiredStringEnvSchema = z.preprocess((value) => {
|
|
|
18
19
|
return normalized.length > 0 ? normalized : undefined;
|
|
19
20
|
}, z.string().min(1));
|
|
20
21
|
|
|
21
|
-
const
|
|
22
|
+
const optionalStringEnvSchema = z.preprocess((value) => {
|
|
22
23
|
if (typeof value !== "string") {
|
|
23
24
|
return value;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const normalized = value.trim();
|
|
27
28
|
return normalized.length > 0 ? normalized : undefined;
|
|
28
|
-
}, z.string().
|
|
29
|
+
}, z.string().min(1).optional());
|
|
29
30
|
|
|
31
|
+
const optionalUrlEnvSchema = z.preprocess((value) => {
|
|
32
|
+
if (typeof value !== "string") {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalized = value.trim();
|
|
37
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
38
|
+
}, z.string().url().optional());
|
|
39
|
+
|
|
40
|
+
const publicContentSourceSchema = z.enum([
|
|
41
|
+
"minutework_cms",
|
|
42
|
+
"custom",
|
|
43
|
+
"static_json",
|
|
44
|
+
"none",
|
|
45
|
+
]);
|
|
30
46
|
const publicSiteEnvironmentSchema = z.enum(["preview", "live"]);
|
|
31
47
|
|
|
32
|
-
const envSchema = z
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const envSchema = z
|
|
49
|
+
.object({
|
|
50
|
+
MW_PLATFORM_BASE_URL: z.string().url(),
|
|
51
|
+
MW_PUBLIC_CONTENT_SOURCE: z.preprocess(
|
|
52
|
+
(value) => value ?? "minutework_cms",
|
|
53
|
+
publicContentSourceSchema,
|
|
54
|
+
),
|
|
55
|
+
MW_CONTENT_API_TOKEN: optionalStringEnvSchema,
|
|
56
|
+
MW_TEMPLATE_APP_NAME: z.preprocess(
|
|
57
|
+
(value) => value ?? "MinuteWork Combined Starter",
|
|
58
|
+
z.string().trim().min(1),
|
|
59
|
+
),
|
|
60
|
+
MW_PUBLIC_BASE_URL: optionalUrlEnvSchema,
|
|
61
|
+
MW_PUBLIC_SITE_PROPERTY_KEY: optionalStringEnvSchema,
|
|
62
|
+
MW_PUBLIC_SITE_ENV: z.preprocess(
|
|
63
|
+
(value) => value ?? "preview",
|
|
64
|
+
publicSiteEnvironmentSchema,
|
|
65
|
+
),
|
|
66
|
+
MW_STATIC_PUBLIC_CONTENT_PATH: z.preprocess(
|
|
67
|
+
(value) => value ?? DEFAULT_STATIC_PUBLIC_CONTENT_PATH,
|
|
68
|
+
requiredStringEnvSchema,
|
|
69
|
+
),
|
|
70
|
+
MW_ENABLE_RUNTIME_COMMAND_EXAMPLE: exampleFlagSchema,
|
|
71
|
+
NODE_ENV: z
|
|
72
|
+
.enum(["development", "test", "production"])
|
|
73
|
+
.default("development"),
|
|
74
|
+
})
|
|
75
|
+
.superRefine((value, context) => {
|
|
76
|
+
if (value.MW_PUBLIC_CONTENT_SOURCE === "minutework_cms") {
|
|
77
|
+
if (!value.MW_CONTENT_API_TOKEN) {
|
|
78
|
+
context.addIssue({
|
|
79
|
+
code: z.ZodIssueCode.custom,
|
|
80
|
+
path: ["MW_CONTENT_API_TOKEN"],
|
|
81
|
+
message: "Required when MW_PUBLIC_CONTENT_SOURCE=minutework_cms",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (!value.MW_PUBLIC_BASE_URL) {
|
|
85
|
+
context.addIssue({
|
|
86
|
+
code: z.ZodIssueCode.custom,
|
|
87
|
+
path: ["MW_PUBLIC_BASE_URL"],
|
|
88
|
+
message: "Required when MW_PUBLIC_CONTENT_SOURCE=minutework_cms",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (!value.MW_PUBLIC_SITE_PROPERTY_KEY) {
|
|
92
|
+
context.addIssue({
|
|
93
|
+
code: z.ZodIssueCode.custom,
|
|
94
|
+
path: ["MW_PUBLIC_SITE_PROPERTY_KEY"],
|
|
95
|
+
message: "Required when MW_PUBLIC_CONTENT_SOURCE=minutework_cms",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
(value.MW_PUBLIC_CONTENT_SOURCE === "custom" ||
|
|
102
|
+
value.MW_PUBLIC_CONTENT_SOURCE === "static_json") &&
|
|
103
|
+
!value.MW_PUBLIC_BASE_URL
|
|
104
|
+
) {
|
|
105
|
+
context.addIssue({
|
|
106
|
+
code: z.ZodIssueCode.custom,
|
|
107
|
+
path: ["MW_PUBLIC_BASE_URL"],
|
|
108
|
+
message: `Required when MW_PUBLIC_CONTENT_SOURCE=${value.MW_PUBLIC_CONTENT_SOURCE}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
50
112
|
|
|
51
113
|
const defaultPlatformBaseUrl =
|
|
52
114
|
process.env.MW_PLATFORM_BASE_URL ??
|
|
@@ -56,11 +118,13 @@ const defaultPlatformBaseUrl =
|
|
|
56
118
|
|
|
57
119
|
const parsedEnv = envSchema.safeParse({
|
|
58
120
|
MW_PLATFORM_BASE_URL: defaultPlatformBaseUrl,
|
|
121
|
+
MW_PUBLIC_CONTENT_SOURCE: process.env.MW_PUBLIC_CONTENT_SOURCE,
|
|
59
122
|
MW_CONTENT_API_TOKEN: process.env.MW_CONTENT_API_TOKEN,
|
|
60
123
|
MW_TEMPLATE_APP_NAME: process.env.MW_TEMPLATE_APP_NAME,
|
|
61
124
|
MW_PUBLIC_BASE_URL: process.env.MW_PUBLIC_BASE_URL,
|
|
62
125
|
MW_PUBLIC_SITE_PROPERTY_KEY: process.env.MW_PUBLIC_SITE_PROPERTY_KEY,
|
|
63
126
|
MW_PUBLIC_SITE_ENV: process.env.MW_PUBLIC_SITE_ENV,
|
|
127
|
+
MW_STATIC_PUBLIC_CONTENT_PATH: process.env.MW_STATIC_PUBLIC_CONTENT_PATH,
|
|
64
128
|
MW_ENABLE_RUNTIME_COMMAND_EXAMPLE:
|
|
65
129
|
process.env.MW_ENABLE_RUNTIME_COMMAND_EXAMPLE,
|
|
66
130
|
NODE_ENV: process.env.NODE_ENV,
|
|
@@ -13,7 +13,7 @@ describe("public-site helpers", () => {
|
|
|
13
13
|
siteName: "MinuteWork Combined Starter",
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
expect(resolvePublicSiteUrl("/docs")
|
|
16
|
+
expect(resolvePublicSiteUrl("/docs")?.toString()).toBe("http://127.0.0.1:3000/docs");
|
|
17
17
|
expect(metadata.alternates?.canonical).toBe("http://127.0.0.1:3000/docs");
|
|
18
18
|
expect(metadata.openGraph?.url).toBe("http://127.0.0.1:3000/docs");
|
|
19
19
|
});
|
|
@@ -4,7 +4,24 @@ import type { Metadata } from "next";
|
|
|
4
4
|
|
|
5
5
|
import { env } from "@/lib/platform/env.server";
|
|
6
6
|
|
|
7
|
+
export function isPublicContentDisabled() {
|
|
8
|
+
return env.MW_PUBLIC_CONTENT_SOURCE === "none";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildDisabledPublicMetadata(): Metadata {
|
|
12
|
+
return {
|
|
13
|
+
robots: {
|
|
14
|
+
index: false,
|
|
15
|
+
follow: false,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
export function resolvePublicMetadataBase() {
|
|
21
|
+
if (!env.MW_PUBLIC_BASE_URL) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
return new URL(
|
|
9
26
|
env.MW_PUBLIC_BASE_URL.endsWith("/")
|
|
10
27
|
? env.MW_PUBLIC_BASE_URL
|
|
@@ -13,7 +30,10 @@ export function resolvePublicMetadataBase() {
|
|
|
13
30
|
}
|
|
14
31
|
|
|
15
32
|
export function resolvePublicSiteUrl(pathname = "/") {
|
|
16
|
-
|
|
33
|
+
const metadataBase = resolvePublicMetadataBase();
|
|
34
|
+
return metadataBase
|
|
35
|
+
? new URL(pathname.replace(/^\/*/, "/"), metadataBase)
|
|
36
|
+
: null;
|
|
17
37
|
}
|
|
18
38
|
|
|
19
39
|
export function buildPublicMetadata(input: {
|
|
@@ -24,20 +44,24 @@ export function buildPublicMetadata(input: {
|
|
|
24
44
|
type?: "website" | "article";
|
|
25
45
|
publishedTime?: string | null;
|
|
26
46
|
}): Metadata {
|
|
27
|
-
const canonicalUrl = resolvePublicSiteUrl(input.path)
|
|
47
|
+
const canonicalUrl = resolvePublicSiteUrl(input.path)?.toString();
|
|
28
48
|
|
|
29
49
|
return {
|
|
30
50
|
title: input.title,
|
|
31
51
|
description: input.description,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
...(canonicalUrl
|
|
53
|
+
? {
|
|
54
|
+
alternates: {
|
|
55
|
+
canonical: canonicalUrl,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
: {}),
|
|
35
59
|
openGraph: {
|
|
36
60
|
title: input.title,
|
|
37
61
|
description: input.description,
|
|
38
|
-
url: canonicalUrl,
|
|
39
62
|
siteName: input.siteName ?? env.MW_TEMPLATE_APP_NAME,
|
|
40
63
|
type: input.type ?? "website",
|
|
64
|
+
...(canonicalUrl ? { url: canonicalUrl } : {}),
|
|
41
65
|
...(input.publishedTime ? { publishedTime: input.publishedTime } : {}),
|
|
42
66
|
},
|
|
43
67
|
twitter: {
|
|
@@ -89,6 +89,7 @@ try {
|
|
|
89
89
|
...process.env,
|
|
90
90
|
MW_CONTENT_API_TOKEN: "validate-content-token",
|
|
91
91
|
MW_PLATFORM_BASE_URL: baseUrl,
|
|
92
|
+
MW_PUBLIC_CONTENT_SOURCE: "minutework_cms",
|
|
92
93
|
MW_PUBLIC_BASE_URL: "https://public.example.com",
|
|
93
94
|
MW_PUBLIC_SITE_ENV: "preview",
|
|
94
95
|
MW_PUBLIC_SITE_PROPERTY_KEY: "main-site",
|
|
@@ -12,10 +12,12 @@ export default defineConfig({
|
|
|
12
12
|
test: {
|
|
13
13
|
env: {
|
|
14
14
|
MW_PLATFORM_BASE_URL: "http://127.0.0.1:8000",
|
|
15
|
+
MW_PUBLIC_CONTENT_SOURCE: "minutework_cms",
|
|
15
16
|
MW_CONTENT_API_TOKEN: "test-content-token",
|
|
16
17
|
MW_PUBLIC_BASE_URL: "http://127.0.0.1:3000",
|
|
17
18
|
MW_PUBLIC_SITE_PROPERTY_KEY: "main-site",
|
|
18
19
|
MW_PUBLIC_SITE_ENV: "preview",
|
|
20
|
+
MW_STATIC_PUBLIC_CONTENT_PATH: "content/public-site.json",
|
|
19
21
|
},
|
|
20
22
|
environment: "node",
|
|
21
23
|
globals: true,
|