minutework 0.1.26 → 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/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
|
@@ -190,8 +190,10 @@ a concrete backend responsibility such as:
|
|
|
190
190
|
`submission` schemas before Builder starts authoring.
|
|
191
191
|
- Author public content against runtime/CMS records and published-web flows, not
|
|
192
192
|
against a fixed in-repo marketing template.
|
|
193
|
-
- Use `
|
|
194
|
-
|
|
193
|
+
- Use `MW_PUBLIC_CONTENT_SOURCE` to select the public content adapter mode.
|
|
194
|
+
- `MW_CONTENT_API_TOKEN`, `MW_PUBLIC_SITE_PROPERTY_KEY`, and
|
|
195
|
+
`MW_PUBLIC_SITE_ENV` are required only for `MW_PUBLIC_CONTENT_SOURCE=minutework_cms`.
|
|
196
|
+
- `MW_STATIC_PUBLIC_CONTENT_PATH` is used for `MW_PUBLIC_CONTENT_SOURCE=static_json`.
|
|
195
197
|
- `MW_PUBLIC_SITE_ENV=preview` is for preview or draft-safe reads.
|
|
196
198
|
- `MW_PUBLIC_SITE_ENV=live` is for publication-safe delivery.
|
|
197
199
|
- Anonymous live delivery should prefer published snapshots instead of direct
|
|
@@ -26,8 +26,10 @@ flows, or the default MinuteWork site model.
|
|
|
26
26
|
`contract-first-public-intake/SKILL.md`.
|
|
27
27
|
- Use `published-web` and hosted public-release flows for anonymous delivery by
|
|
28
28
|
default.
|
|
29
|
+
- `MW_PUBLIC_CONTENT_SOURCE` selects the public content adapter mode.
|
|
29
30
|
- `MW_CONTENT_API_TOKEN`, `MW_PUBLIC_SITE_PROPERTY_KEY`, and
|
|
30
|
-
`MW_PUBLIC_SITE_ENV` are
|
|
31
|
+
`MW_PUBLIC_SITE_ENV` are required only for `MW_PUBLIC_CONTENT_SOURCE=minutework_cms`.
|
|
32
|
+
- `MW_STATIC_PUBLIC_CONTENT_PATH` is used for `MW_PUBLIC_CONTENT_SOURCE=static_json`.
|
|
31
33
|
- `MW_PUBLIC_SITE_ENV=preview` is for preview or draft-safe reads.
|
|
32
34
|
- `MW_PUBLIC_SITE_ENV=live` is for publication-safe reads only.
|
|
33
35
|
- Anonymous live traffic should prefer published snapshots instead of direct
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
MW_PLATFORM_BASE_URL=http://127.0.0.1:8000
|
|
2
|
+
MW_PUBLIC_CONTENT_SOURCE=none
|
|
2
3
|
MW_CONTENT_API_TOKEN=
|
|
3
4
|
MW_TEMPLATE_APP_NAME=MinuteWork Combined Starter
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
MW_PUBLIC_SITE_PROPERTY_KEY=main-site
|
|
5
|
+
MW_PUBLIC_BASE_URL=
|
|
6
|
+
MW_PUBLIC_SITE_PROPERTY_KEY=
|
|
7
7
|
MW_PUBLIC_SITE_ENV=preview
|
|
8
|
+
MW_STATIC_PUBLIC_CONTENT_PATH=content/public-site.json
|
|
8
9
|
MW_ENABLE_RUNTIME_COMMAND_EXAMPLE=false
|
|
@@ -89,14 +89,19 @@ When disabled:
|
|
|
89
89
|
|
|
90
90
|
## Environment
|
|
91
91
|
|
|
92
|
-
Copy `.env.example` to `.env.local` when running the template directly.
|
|
92
|
+
Copy `.env.example` to `.env.local` when running the template directly. The
|
|
93
|
+
example file uses `MW_PUBLIC_CONTENT_SOURCE=none` so the private `/app` surface
|
|
94
|
+
can boot without MinuteWork CMS credentials; set `MW_PUBLIC_CONTENT_SOURCE` to
|
|
95
|
+
`minutework_cms` and fill the CMS values below when enabling the public site.
|
|
93
96
|
|
|
94
97
|
- `MW_PLATFORM_BASE_URL` is required
|
|
95
|
-
- `
|
|
98
|
+
- `MW_PUBLIC_CONTENT_SOURCE` defaults to `minutework_cms` when omitted; supported values are `minutework_cms`, `custom`, `static_json`, and `none`
|
|
99
|
+
- `MW_CONTENT_API_TOKEN` is required only for `minutework_cms` and must stay server-only
|
|
96
100
|
- `MW_TEMPLATE_APP_NAME` defaults to `MinuteWork Combined Starter`
|
|
97
|
-
- `MW_PUBLIC_BASE_URL` is required and should match the deployed public origin
|
|
98
|
-
- `MW_PUBLIC_SITE_PROPERTY_KEY` is required and selects the `PublishedWebProperty` backing the site
|
|
101
|
+
- `MW_PUBLIC_BASE_URL` is required for public content sources and should match the deployed public origin; it may be omitted when `MW_PUBLIC_CONTENT_SOURCE=none`
|
|
102
|
+
- `MW_PUBLIC_SITE_PROPERTY_KEY` is required only for `minutework_cms` and selects the `PublishedWebProperty` backing the site
|
|
99
103
|
- `MW_PUBLIC_SITE_ENV` defaults to `preview`
|
|
104
|
+
- `MW_STATIC_PUBLIC_CONTENT_PATH` defaults to `content/public-site.json` and is used when `MW_PUBLIC_CONTENT_SOURCE=static_json`
|
|
100
105
|
- `MW_ENABLE_RUNTIME_COMMAND_EXAMPLE` defaults to `false`
|
|
101
106
|
|
|
102
107
|
## SEO baseline
|
|
@@ -4,11 +4,19 @@ import { notFound } from "next/navigation";
|
|
|
4
4
|
import { ContentStructureRenderer } from "@/features/public-shell/components/section-renderer";
|
|
5
5
|
import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
|
|
6
6
|
import { getPageByPath, getSiteConfig, listPages } from "@/lib/content/adapter.server";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildDisabledPublicMetadata,
|
|
9
|
+
buildPublicMetadata,
|
|
10
|
+
isPublicContentDisabled,
|
|
11
|
+
} from "@/lib/public-site";
|
|
8
12
|
|
|
9
13
|
type PageParams = { path: string[] };
|
|
10
14
|
|
|
11
15
|
export async function generateStaticParams(): Promise<PageParams[]> {
|
|
16
|
+
if (isPublicContentDisabled()) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
const pages = await listPages("published");
|
|
13
21
|
return pages
|
|
14
22
|
.filter((p) => p.path !== "/")
|
|
@@ -22,6 +30,10 @@ export async function generateMetadata({
|
|
|
22
30
|
}: {
|
|
23
31
|
params: Promise<PageParams>;
|
|
24
32
|
}): Promise<Metadata> {
|
|
33
|
+
if (isPublicContentDisabled()) {
|
|
34
|
+
return buildDisabledPublicMetadata();
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
const { path: pathSegments } = await params;
|
|
26
38
|
const pagePath = `/${pathSegments.join("/")}`;
|
|
27
39
|
const page = await getPageByPath(pagePath);
|
|
@@ -41,6 +53,10 @@ export default async function CmsPage({
|
|
|
41
53
|
}: {
|
|
42
54
|
params: Promise<PageParams>;
|
|
43
55
|
}) {
|
|
56
|
+
if (isPublicContentDisabled()) {
|
|
57
|
+
notFound();
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
const { path: pathSegments } = await params;
|
|
45
61
|
const pagePath = `/${pathSegments.join("/")}`;
|
|
46
62
|
const [siteConfig, page] = await Promise.all([
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const originalEnv = { ...process.env };
|
|
4
|
+
const resolveAuthenticatedSession = vi.fn();
|
|
5
|
+
|
|
6
|
+
function restoreProcessEnv() {
|
|
7
|
+
for (const key of Object.keys(process.env)) {
|
|
8
|
+
if (!(key in originalEnv)) {
|
|
9
|
+
delete process.env[key];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
Object.assign(process.env, originalEnv);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function applyPrivateOnlyServerEnv() {
|
|
17
|
+
restoreProcessEnv();
|
|
18
|
+
process.env.MW_PLATFORM_BASE_URL = "http://127.0.0.1:8000";
|
|
19
|
+
process.env.MW_PUBLIC_CONTENT_SOURCE = "none";
|
|
20
|
+
process.env.MW_TEMPLATE_APP_NAME = "Private Only Workspace";
|
|
21
|
+
process.env.MW_ENABLE_RUNTIME_COMMAND_EXAMPLE = "false";
|
|
22
|
+
delete process.env.MW_CONTENT_API_TOKEN;
|
|
23
|
+
delete process.env.MW_PUBLIC_BASE_URL;
|
|
24
|
+
delete process.env.MW_PUBLIC_SITE_PROPERTY_KEY;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
vi.mock("@/features/shell/components/private-app-shell", () => ({
|
|
28
|
+
PrivateAppShell: () => null,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock("@/lib/platform/auth.server", () => ({
|
|
32
|
+
resolveAuthenticatedSession,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("@/lib/platform/endpoints.server", () => ({
|
|
36
|
+
platformAuthEndpoints: {
|
|
37
|
+
operatorConsole: "http://127.0.0.1:8000/ops/login/",
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
describe("private app routes with disabled public content", () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
vi.resetModules();
|
|
45
|
+
applyPrivateOnlyServerEnv();
|
|
46
|
+
resolveAuthenticatedSession.mockResolvedValue({
|
|
47
|
+
authenticated: true,
|
|
48
|
+
user: {
|
|
49
|
+
id: "user-1",
|
|
50
|
+
username: "demo-user",
|
|
51
|
+
email: "demo@example.com",
|
|
52
|
+
},
|
|
53
|
+
active_tenant_id: "tenant-1",
|
|
54
|
+
active_tenant: {
|
|
55
|
+
tenant_id: "tenant-1",
|
|
56
|
+
tenant_slug: "alpha",
|
|
57
|
+
tenant_name: "Alpha",
|
|
58
|
+
role: "admin",
|
|
59
|
+
},
|
|
60
|
+
memberships: [
|
|
61
|
+
{
|
|
62
|
+
tenant_id: "tenant-1",
|
|
63
|
+
tenant_slug: "alpha",
|
|
64
|
+
tenant_name: "Alpha",
|
|
65
|
+
role: "admin",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
restoreProcessEnv();
|
|
73
|
+
vi.resetModules();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("imports /app entrypoints without CMS or public base environment", async () => {
|
|
77
|
+
const [layout, page] = await Promise.all([
|
|
78
|
+
import("./layout"),
|
|
79
|
+
import("./page"),
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
await expect(layout.default({ children: "workspace" })).resolves.toBe(
|
|
83
|
+
"workspace",
|
|
84
|
+
);
|
|
85
|
+
await expect(page.default()).resolves.toBeDefined();
|
|
86
|
+
expect(resolveAuthenticatedSession).toHaveBeenCalledTimes(2);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -4,7 +4,11 @@ import { ContentArticle } from "@/features/public-shell/components/content-artic
|
|
|
4
4
|
import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
|
|
5
5
|
import { getEntry, getSiteConfig, listEntries } from "@/lib/content/adapter.server";
|
|
6
6
|
import { appRoutes } from "@/lib/app-routes";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildDisabledPublicMetadata,
|
|
9
|
+
buildPublicMetadata,
|
|
10
|
+
isPublicContentDisabled,
|
|
11
|
+
} from "@/lib/public-site";
|
|
8
12
|
|
|
9
13
|
type BlogArticlePageProps = {
|
|
10
14
|
params: Promise<{ slug: string }>;
|
|
@@ -13,12 +17,20 @@ type BlogArticlePageProps = {
|
|
|
13
17
|
export const dynamicParams = false;
|
|
14
18
|
|
|
15
19
|
export async function generateStaticParams() {
|
|
20
|
+
if (isPublicContentDisabled()) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
return (await listEntries("blog")).map((entry) => ({
|
|
17
25
|
slug: entry.slug[0],
|
|
18
26
|
}));
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
export async function generateMetadata({ params }: BlogArticlePageProps) {
|
|
30
|
+
if (isPublicContentDisabled()) {
|
|
31
|
+
return buildDisabledPublicMetadata();
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
const [{ slug }, siteConfig] = await Promise.all([params, getSiteConfig()]);
|
|
23
35
|
const entry = await getEntry("blog", [slug]);
|
|
24
36
|
|
|
@@ -42,6 +54,10 @@ export async function generateMetadata({ params }: BlogArticlePageProps) {
|
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
export default async function BlogArticlePage({ params }: BlogArticlePageProps) {
|
|
57
|
+
if (isPublicContentDisabled()) {
|
|
58
|
+
notFound();
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
const [{ slug }, siteConfig] = await Promise.all([params, getSiteConfig()]);
|
|
46
62
|
const entry = await getEntry("blog", [slug]);
|
|
47
63
|
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
|
|
1
3
|
import { ContentCollection } from "@/features/public-shell/components/content-collection";
|
|
2
4
|
import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
|
|
3
5
|
import { getSiteConfig, listEntries } from "@/lib/content/adapter.server";
|
|
4
6
|
import { appRoutes } from "@/lib/app-routes";
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildDisabledPublicMetadata,
|
|
9
|
+
buildPublicMetadata,
|
|
10
|
+
isPublicContentDisabled,
|
|
11
|
+
} from "@/lib/public-site";
|
|
6
12
|
|
|
7
13
|
export async function generateMetadata() {
|
|
14
|
+
if (isPublicContentDisabled()) {
|
|
15
|
+
return buildDisabledPublicMetadata();
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
const siteConfig = await getSiteConfig();
|
|
9
19
|
const collection = siteConfig.collections.blog;
|
|
10
20
|
|
|
@@ -17,6 +27,10 @@ export async function generateMetadata() {
|
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
export default async function BlogIndexPage() {
|
|
30
|
+
if (isPublicContentDisabled()) {
|
|
31
|
+
notFound();
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
const [siteConfig, entries] = await Promise.all([
|
|
21
35
|
getSiteConfig(),
|
|
22
36
|
listEntries("blog"),
|
|
@@ -4,7 +4,11 @@ import { ContentArticle } from "@/features/public-shell/components/content-artic
|
|
|
4
4
|
import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
|
|
5
5
|
import { getEntry, getSiteConfig, listEntries } from "@/lib/content/adapter.server";
|
|
6
6
|
import { appRoutes } from "@/lib/app-routes";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildDisabledPublicMetadata,
|
|
9
|
+
buildPublicMetadata,
|
|
10
|
+
isPublicContentDisabled,
|
|
11
|
+
} from "@/lib/public-site";
|
|
8
12
|
|
|
9
13
|
type DocsArticlePageProps = {
|
|
10
14
|
params: Promise<{ slug: string[] }>;
|
|
@@ -13,12 +17,20 @@ type DocsArticlePageProps = {
|
|
|
13
17
|
export const dynamicParams = false;
|
|
14
18
|
|
|
15
19
|
export async function generateStaticParams() {
|
|
20
|
+
if (isPublicContentDisabled()) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
return (await listEntries("docs")).map((entry) => ({
|
|
17
25
|
slug: entry.slug,
|
|
18
26
|
}));
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
export async function generateMetadata({ params }: DocsArticlePageProps) {
|
|
30
|
+
if (isPublicContentDisabled()) {
|
|
31
|
+
return buildDisabledPublicMetadata();
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
const [{ slug }, siteConfig] = await Promise.all([params, getSiteConfig()]);
|
|
23
35
|
const entry = await getEntry("docs", slug);
|
|
24
36
|
|
|
@@ -40,6 +52,10 @@ export async function generateMetadata({ params }: DocsArticlePageProps) {
|
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
export default async function DocsArticlePage({ params }: DocsArticlePageProps) {
|
|
55
|
+
if (isPublicContentDisabled()) {
|
|
56
|
+
notFound();
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
const [{ slug }, siteConfig] = await Promise.all([params, getSiteConfig()]);
|
|
44
60
|
const entry = await getEntry("docs", slug);
|
|
45
61
|
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
|
|
1
3
|
import { ContentCollection } from "@/features/public-shell/components/content-collection";
|
|
2
4
|
import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
|
|
3
5
|
import { getSiteConfig, listEntries } from "@/lib/content/adapter.server";
|
|
4
6
|
import { appRoutes } from "@/lib/app-routes";
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildDisabledPublicMetadata,
|
|
9
|
+
buildPublicMetadata,
|
|
10
|
+
isPublicContentDisabled,
|
|
11
|
+
} from "@/lib/public-site";
|
|
6
12
|
|
|
7
13
|
export async function generateMetadata() {
|
|
14
|
+
if (isPublicContentDisabled()) {
|
|
15
|
+
return buildDisabledPublicMetadata();
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
const siteConfig = await getSiteConfig();
|
|
9
19
|
const collection = siteConfig.collections.docs;
|
|
10
20
|
|
|
@@ -17,6 +27,10 @@ export async function generateMetadata() {
|
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
export default async function DocsIndexPage() {
|
|
30
|
+
if (isPublicContentDisabled()) {
|
|
31
|
+
notFound();
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
const [siteConfig, entries] = await Promise.all([
|
|
21
35
|
getSiteConfig(),
|
|
22
36
|
listEntries("docs"),
|
|
@@ -24,8 +24,10 @@ const geistMono = Geist_Mono({
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
export function generateMetadata(): Metadata {
|
|
27
|
+
const metadataBase = resolvePublicMetadataBase();
|
|
28
|
+
|
|
27
29
|
return {
|
|
28
|
-
metadataBase:
|
|
30
|
+
...(metadataBase ? { metadataBase } : {}),
|
|
29
31
|
title: {
|
|
30
32
|
default: env.MW_TEMPLATE_APP_NAME,
|
|
31
33
|
template: `%s | ${env.MW_TEMPLATE_APP_NAME}`,
|
|
@@ -36,9 +38,9 @@ export function generateMetadata(): Metadata {
|
|
|
36
38
|
title: env.MW_TEMPLATE_APP_NAME,
|
|
37
39
|
description:
|
|
38
40
|
"SEO-first public and authenticated Next.js starter with a server-owned platform session BFF.",
|
|
39
|
-
url: env.MW_PUBLIC_BASE_URL,
|
|
40
41
|
siteName: env.MW_TEMPLATE_APP_NAME,
|
|
41
42
|
type: "website",
|
|
43
|
+
...(env.MW_PUBLIC_BASE_URL ? { url: env.MW_PUBLIC_BASE_URL } : {}),
|
|
42
44
|
},
|
|
43
45
|
};
|
|
44
46
|
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
|
|
1
3
|
import { MarketingPageCanvas } from "@/features/public-shell/components/marketing-page-canvas";
|
|
2
4
|
import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
|
|
3
5
|
import { getMarketingPage, getSiteConfig } from "@/lib/content/adapter.server";
|
|
4
6
|
import { buildEmptyMarketingPage } from "@/lib/content/empty-state";
|
|
5
7
|
import { appRoutes } from "@/lib/app-routes";
|
|
6
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildDisabledPublicMetadata,
|
|
10
|
+
buildPublicMetadata,
|
|
11
|
+
isPublicContentDisabled,
|
|
12
|
+
} from "@/lib/public-site";
|
|
7
13
|
|
|
8
14
|
export async function generateMetadata() {
|
|
15
|
+
if (isPublicContentDisabled()) {
|
|
16
|
+
return buildDisabledPublicMetadata();
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
const [siteConfig, page] = await Promise.all([
|
|
10
20
|
getSiteConfig(),
|
|
11
21
|
getMarketingPage("home"),
|
|
@@ -21,6 +31,10 @@ export async function generateMetadata() {
|
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
export default async function HomePage() {
|
|
34
|
+
if (isPublicContentDisabled()) {
|
|
35
|
+
notFound();
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
const [siteConfig, page] = await Promise.all([
|
|
25
39
|
getSiteConfig(),
|
|
26
40
|
getMarketingPage("home"),
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
|
|
1
3
|
import { MarketingPageCanvas } from "@/features/public-shell/components/marketing-page-canvas";
|
|
2
4
|
import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
|
|
3
5
|
import { getMarketingPage, getSiteConfig } from "@/lib/content/adapter.server";
|
|
4
6
|
import { buildEmptyMarketingPage } from "@/lib/content/empty-state";
|
|
5
7
|
import { appRoutes } from "@/lib/app-routes";
|
|
6
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildDisabledPublicMetadata,
|
|
10
|
+
buildPublicMetadata,
|
|
11
|
+
isPublicContentDisabled,
|
|
12
|
+
} from "@/lib/public-site";
|
|
7
13
|
|
|
8
14
|
export async function generateMetadata() {
|
|
15
|
+
if (isPublicContentDisabled()) {
|
|
16
|
+
return buildDisabledPublicMetadata();
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
const [siteConfig, page] = await Promise.all([
|
|
10
20
|
getSiteConfig(),
|
|
11
21
|
getMarketingPage("pricing"),
|
|
@@ -21,6 +31,10 @@ export async function generateMetadata() {
|
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
export default async function PricingPage() {
|
|
34
|
+
if (isPublicContentDisabled()) {
|
|
35
|
+
notFound();
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
const [siteConfig, page] = await Promise.all([
|
|
25
39
|
getSiteConfig(),
|
|
26
40
|
getMarketingPage("pricing"),
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import robots from "./robots";
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
2
|
|
|
5
3
|
describe("robots route", () => {
|
|
6
|
-
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.resetModules();
|
|
6
|
+
vi.doUnmock("@/lib/public-site");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("returns a public robots policy and sitemap url", async () => {
|
|
7
10
|
expect(process.env.MW_PUBLIC_BASE_URL).toBe("http://127.0.0.1:3000");
|
|
8
11
|
|
|
12
|
+
const { default: robots } = await import("./robots");
|
|
9
13
|
const metadata = robots();
|
|
10
14
|
|
|
11
15
|
expect(metadata.host).toBe("127.0.0.1:3000");
|
|
@@ -17,4 +21,20 @@ describe("robots route", () => {
|
|
|
17
21
|
},
|
|
18
22
|
]);
|
|
19
23
|
});
|
|
24
|
+
|
|
25
|
+
it("disallows indexing when public content is disabled", async () => {
|
|
26
|
+
vi.doMock("@/lib/public-site", () => ({
|
|
27
|
+
isPublicContentDisabled: () => true,
|
|
28
|
+
resolvePublicMetadataBase: () => null,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const { default: robots } = await import("./robots");
|
|
32
|
+
|
|
33
|
+
expect(robots().rules).toEqual([
|
|
34
|
+
{
|
|
35
|
+
userAgent: "*",
|
|
36
|
+
disallow: "/",
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
20
40
|
});
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import type { MetadataRoute } from "next";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
isPublicContentDisabled,
|
|
5
|
+
resolvePublicMetadataBase,
|
|
6
|
+
} from "@/lib/public-site";
|
|
4
7
|
|
|
5
8
|
export default function robots(): MetadataRoute.Robots {
|
|
6
9
|
const metadataBase = resolvePublicMetadataBase();
|
|
7
10
|
|
|
11
|
+
if (!metadataBase || isPublicContentDisabled()) {
|
|
12
|
+
return {
|
|
13
|
+
rules: [
|
|
14
|
+
{
|
|
15
|
+
userAgent: "*",
|
|
16
|
+
disallow: "/",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
return {
|
|
9
23
|
rules: [
|
|
10
24
|
{
|
|
@@ -8,11 +8,11 @@ vi.mock("@/lib/content/adapter.server", () => ({
|
|
|
8
8
|
listEntries,
|
|
9
9
|
}));
|
|
10
10
|
|
|
11
|
-
import sitemap from "./sitemap";
|
|
12
|
-
|
|
13
11
|
describe("sitemap route", () => {
|
|
14
12
|
beforeEach(() => {
|
|
15
13
|
vi.clearAllMocks();
|
|
14
|
+
vi.resetModules();
|
|
15
|
+
vi.doUnmock("@/lib/public-site");
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
it("includes the public route surface and published content entries", async () => {
|
|
@@ -24,6 +24,7 @@ describe("sitemap route", () => {
|
|
|
24
24
|
|
|
25
25
|
expect(process.env.MW_PUBLIC_BASE_URL).toBe("http://127.0.0.1:3000");
|
|
26
26
|
|
|
27
|
+
const { default: sitemap } = await import("./sitemap");
|
|
27
28
|
const entries = await sitemap();
|
|
28
29
|
const urls = entries.map((entry) => entry.url);
|
|
29
30
|
|
|
@@ -38,6 +39,7 @@ describe("sitemap route", () => {
|
|
|
38
39
|
it("keeps home and pricing in the sitemap even when the snapshot has no marketing pages", async () => {
|
|
39
40
|
listEntries.mockResolvedValue([]);
|
|
40
41
|
|
|
42
|
+
const { default: sitemap } = await import("./sitemap");
|
|
41
43
|
const entries = await sitemap();
|
|
42
44
|
const urls = entries.map((entry) => entry.url);
|
|
43
45
|
|
|
@@ -46,4 +48,16 @@ describe("sitemap route", () => {
|
|
|
46
48
|
expect(urls).toContain("http://127.0.0.1:3000/docs");
|
|
47
49
|
expect(urls).toContain("http://127.0.0.1:3000/blog");
|
|
48
50
|
});
|
|
51
|
+
|
|
52
|
+
it("returns an empty sitemap when public content is disabled", async () => {
|
|
53
|
+
vi.doMock("@/lib/public-site", () => ({
|
|
54
|
+
isPublicContentDisabled: () => true,
|
|
55
|
+
resolvePublicSiteUrl: vi.fn(),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
const { default: sitemap } = await import("./sitemap");
|
|
59
|
+
|
|
60
|
+
await expect(sitemap()).resolves.toEqual([]);
|
|
61
|
+
expect(listEntries).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
49
63
|
});
|