minutework 0.1.26 → 0.1.28

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.
Files changed (30) hide show
  1. package/assets/claude-local/CLAUDE.md.template +4 -2
  2. package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +3 -1
  3. package/assets/templates/next-tenant-app/.env.example +4 -3
  4. package/assets/templates/next-tenant-app/README.md +9 -4
  5. package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +17 -1
  6. package/assets/templates/next-tenant-app/src/app/app/private-content-source.test.ts +88 -0
  7. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +17 -1
  8. package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +4 -0
  9. package/assets/templates/next-tenant-app/src/app/blog/page.tsx +15 -1
  10. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +17 -1
  11. package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +4 -0
  12. package/assets/templates/next-tenant-app/src/app/docs/page.tsx +15 -1
  13. package/assets/templates/next-tenant-app/src/app/layout.tsx +4 -2
  14. package/assets/templates/next-tenant-app/src/app/page.test.ts +4 -0
  15. package/assets/templates/next-tenant-app/src/app/page.tsx +15 -1
  16. package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +4 -0
  17. package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +15 -1
  18. package/assets/templates/next-tenant-app/src/app/robots.test.ts +24 -4
  19. package/assets/templates/next-tenant-app/src/app/robots.ts +15 -1
  20. package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +16 -2
  21. package/assets/templates/next-tenant-app/src/app/sitemap.ts +21 -7
  22. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +102 -0
  23. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +140 -35
  24. package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +109 -0
  25. package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +84 -20
  26. package/assets/templates/next-tenant-app/src/lib/public-site.test.ts +1 -1
  27. package/assets/templates/next-tenant-app/src/lib/public-site.ts +30 -6
  28. package/assets/templates/next-tenant-app/tools/template/with-public-site-fixture.mjs +1 -0
  29. package/assets/templates/next-tenant-app/vitest.config.ts +2 -0
  30. 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 requiredUrlEnvSchema = z.preprocess((value) => {
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().url());
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.object({
33
- MW_PLATFORM_BASE_URL: z.string().url(),
34
- MW_CONTENT_API_TOKEN: requiredStringEnvSchema,
35
- MW_TEMPLATE_APP_NAME: z.preprocess(
36
- (value) => value ?? "MinuteWork Combined Starter",
37
- z.string().trim().min(1),
38
- ),
39
- MW_PUBLIC_BASE_URL: requiredUrlEnvSchema,
40
- MW_PUBLIC_SITE_PROPERTY_KEY: requiredStringEnvSchema,
41
- MW_PUBLIC_SITE_ENV: z.preprocess(
42
- (value) => value ?? "preview",
43
- publicSiteEnvironmentSchema,
44
- ),
45
- MW_ENABLE_RUNTIME_COMMAND_EXAMPLE: exampleFlagSchema,
46
- NODE_ENV: z
47
- .enum(["development", "test", "production"])
48
- .default("development"),
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 ?? "none",
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").toString()).toBe("http://127.0.0.1:3000/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
- return new URL(pathname.replace(/^\/*/, "/"), resolvePublicMetadataBase());
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).toString();
47
+ const canonicalUrl = resolvePublicSiteUrl(input.path)?.toString();
28
48
 
29
49
  return {
30
50
  title: input.title,
31
51
  description: input.description,
32
- alternates: {
33
- canonical: canonicalUrl,
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minutework",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "MinuteWork CLI for workspace scaffolding, local preview workflows, and hosted preview deploys.",
5
5
  "type": "module",
6
6
  "bin": {