@pantheon-systems/create-p1-starter-kit 0.13.0 → 0.14.0
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/package.json +5 -3
- package/template/__tests__/chatbot-flag-context.test.ts +55 -0
- package/template/__tests__/data-list-block-sub-components.test.tsx +246 -0
- package/template/__tests__/data-list-block-utils.test.ts +84 -0
- package/template/__tests__/data-list-block.test.ts +198 -0
- package/template/__tests__/published-page-routes.test.ts +86 -0
- package/template/app/[...puckPath]/page.tsx +10 -117
- package/template/app/p1/(editor)/[[...p1]]/editor-client.tsx +23 -73
- package/template/app/page.tsx +11 -69
- package/template/app/published-pages.tsx +23 -0
- package/template/app/welcome-block.tsx +25 -0
- package/template/components/ChatbotFlagProvider.tsx +3 -8
- package/template/components/p1-lockup.tsx +2 -1
- package/template/components/puck/image-block.tsx +5 -2
- package/template/components/puck/welcome-block-render.tsx +30 -37
- package/template/components/welcome-block.module.css +124 -0
- package/template/eslint.config.js +9 -1
- package/template/lib/chatbot-flag/flag-context.ts +32 -0
- package/template/package.json +3 -3
- package/template/vitest.config.ts +17 -20
- package/template/__tests__/published-page-404.test.ts +0 -65
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The render pipeline itself is tested in the SDK (create-published-page.test.tsx).
|
|
3
|
+
* What matters here is the wiring: that both routes are shims over the factory,
|
|
4
|
+
* and that the segment config Next.js statically analyzes survives in the files.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
import { resolve, dirname } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { describe, expect, it, vi } from "vitest";
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const starterDir = resolve(__dirname, "..");
|
|
14
|
+
|
|
15
|
+
const factory = vi.hoisted(() => ({
|
|
16
|
+
Page: () => null,
|
|
17
|
+
HomePage: () => null,
|
|
18
|
+
generateMetadata: vi.fn(),
|
|
19
|
+
generateHomeMetadata: vi.fn(),
|
|
20
|
+
generateStaticParams: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("@pantheon-systems/p1-next-sdk/server", () => ({
|
|
24
|
+
createPublishedPage: vi.fn(() => factory),
|
|
25
|
+
loadRouteTemplateKeys: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// The routes reach these through app/published-pages.tsx. Mocked so this stays
|
|
29
|
+
// a wiring test: importing the real modules drags in the whole datasource graph,
|
|
30
|
+
// which is not installed when the built template's copy of this test runs
|
|
31
|
+
// inside the create-p1-starter-kit package.
|
|
32
|
+
vi.mock("../lib/remote-datasource-fetchers", () => ({
|
|
33
|
+
REMOTE_DATASOURCE_FETCHERS: [],
|
|
34
|
+
}));
|
|
35
|
+
vi.mock("../lib/page-seo", () => ({ resolvePageMetadata: vi.fn() }));
|
|
36
|
+
|
|
37
|
+
vi.mock("../app/[...puckPath]/client", () => ({ Client: () => null }));
|
|
38
|
+
vi.mock("../components/puck/welcome-block-render", () => ({
|
|
39
|
+
WelcomeBlockRender: () => null,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const routes = {
|
|
43
|
+
catchAll: "app/[...puckPath]/page.tsx",
|
|
44
|
+
home: "app/page.tsx",
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
describe("published page routes are shims over the SDK factory", () => {
|
|
48
|
+
it("wires the catch-all route to the factory", async () => {
|
|
49
|
+
const route = await import("../app/[...puckPath]/page");
|
|
50
|
+
expect(route.default).toBe(factory.Page);
|
|
51
|
+
expect(route.generateMetadata).toBe(factory.generateMetadata);
|
|
52
|
+
expect(route.generateStaticParams).toBe(factory.generateStaticParams);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("wires the home route to the factory", async () => {
|
|
56
|
+
const route = await import("../app/page");
|
|
57
|
+
expect(route.default).toBe(factory.HomePage);
|
|
58
|
+
expect(route.generateMetadata).toBe(factory.generateHomeMetadata);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Next.js statically analyzes segment-config exports, so this value cannot be
|
|
62
|
+
// re-exported through the factory — a computed or forwarded `revalidate` goes
|
|
63
|
+
// undetected and the route silently loses its revalidation window.
|
|
64
|
+
it.each(Object.values(routes))(
|
|
65
|
+
"keeps revalidate a literal export in %s",
|
|
66
|
+
(file) => {
|
|
67
|
+
const source = readFileSync(resolve(starterDir, file), "utf-8");
|
|
68
|
+
expect(source).toMatch(/^export const revalidate = \d+;$/m);
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// The pipeline is the SDK's now; a copy growing back here is the regression
|
|
73
|
+
// this guards against.
|
|
74
|
+
it.each(Object.values(routes))("leaves no pipeline logic in %s", (file) => {
|
|
75
|
+
const source = readFileSync(resolve(starterDir, file), "utf-8");
|
|
76
|
+
for (const symbol of [
|
|
77
|
+
"loadPublishedPage",
|
|
78
|
+
"loadRemoteDatasourceContext",
|
|
79
|
+
"resolveDataTemplates",
|
|
80
|
+
"isInternalPath",
|
|
81
|
+
"notFound",
|
|
82
|
+
]) {
|
|
83
|
+
expect(source).not.toContain(symbol);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -1,130 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Catch-all route that renders user-facing pages generated by Puck.
|
|
3
|
+
*
|
|
4
|
+
* The pipeline lives in the SDK — see createPublishedPage in
|
|
5
|
+
* app/published-pages.tsx for what this route is made of.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
|
-
import {
|
|
6
|
-
import type { Metadata } from "next";
|
|
7
|
-
import { notFound } from "next/navigation";
|
|
8
|
-
import {
|
|
9
|
-
loadRemoteDatasourceContext,
|
|
10
|
-
extractReferencedDatasourceIds,
|
|
11
|
-
resolveDataTemplates,
|
|
12
|
-
pagePathFromCatchAllSegments,
|
|
13
|
-
} from "@pantheon-systems/puck-css/server";
|
|
14
|
-
import {
|
|
15
|
-
createCssQueryFetchers,
|
|
16
|
-
loadPublishedPage,
|
|
17
|
-
loadRouteTemplateKeys,
|
|
18
|
-
} from "@pantheon-systems/p1-next-sdk/server";
|
|
19
|
-
import { REMOTE_DATASOURCE_FETCHERS } from "../../lib/remote-datasource-fetchers";
|
|
20
|
-
import { ContentUnavailable } from "../../components/content-unavailable";
|
|
21
|
-
import { resolvePageMetadata } from "../../lib/page-seo";
|
|
22
|
-
import { Client } from "./client";
|
|
23
|
-
|
|
24
|
-
const getCcrQueryFetchers = cache(() => createCssQueryFetchers());
|
|
25
|
-
|
|
26
|
-
// Document namespaces that live alongside pages but are never routable.
|
|
27
|
-
const INTERNAL_PATH_PREFIXES = ["/_registry", "/_redirects"];
|
|
28
|
-
|
|
29
|
-
// Lowercased to match the server, which normalizes document paths to lower case
|
|
30
|
-
// before looking them up — so /_Redirects/x resolves the same record as
|
|
31
|
-
// /_redirects/x and must be refused just the same.
|
|
32
|
-
function isInternalPath(path: string): boolean {
|
|
33
|
-
const normalized = path.toLowerCase();
|
|
34
|
-
return INTERNAL_PATH_PREFIXES.some(
|
|
35
|
-
(prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`),
|
|
36
|
-
);
|
|
37
|
-
}
|
|
8
|
+
import { published } from "../published-pages";
|
|
38
9
|
|
|
39
10
|
/**
|
|
40
11
|
* Backstop only: publishing calls revalidatePath for the affected routes, so
|
|
41
12
|
* cached pages normally refresh the moment their content changes. This bounds
|
|
42
13
|
* how long an edit made outside that path (a direct API write, a restored
|
|
43
14
|
* branch) can stay stale.
|
|
15
|
+
*
|
|
16
|
+
* Must stay a literal here: Next.js statically analyzes segment-config exports,
|
|
17
|
+
* so a value re-exported from the factory would go undetected.
|
|
44
18
|
*/
|
|
45
19
|
export const revalidate = 300;
|
|
46
20
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* renderable at all; without it every path here renders fully dynamically and
|
|
51
|
-
* no response is ever cacheable. Unlisted paths render on first request and are
|
|
52
|
-
* cached from then on (dynamicParams defaults to true).
|
|
53
|
-
*/
|
|
54
|
-
export function generateStaticParams(): { puckPath: string[] }[] {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export async function generateMetadata({
|
|
59
|
-
params,
|
|
60
|
-
}: {
|
|
61
|
-
params: Promise<{ puckPath: string[] }>;
|
|
62
|
-
}): Promise<Metadata> {
|
|
63
|
-
const { puckPath = [] } = await params;
|
|
64
|
-
const path = pagePathFromCatchAllSegments(puckPath);
|
|
65
|
-
|
|
66
|
-
if (isInternalPath(path)) {
|
|
67
|
-
return { title: "Not Found" };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const result = await loadPublishedPage(path);
|
|
71
|
-
if (result.status === "missing") return { title: "Not Found" };
|
|
72
|
-
if (result.status === "unavailable") {
|
|
73
|
-
return { title: "Temporarily unavailable" };
|
|
74
|
-
}
|
|
75
|
-
return resolvePageMetadata({ pageData: result.data, path });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export default async function Page({
|
|
79
|
-
params,
|
|
80
|
-
}: {
|
|
81
|
-
params: Promise<{ puckPath: string[] }>;
|
|
82
|
-
}) {
|
|
83
|
-
const { puckPath = [] } = await params;
|
|
84
|
-
const path = pagePathFromCatchAllSegments(puckPath);
|
|
85
|
-
|
|
86
|
-
if (isInternalPath(path)) {
|
|
87
|
-
notFound();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const result = await loadPublishedPage(path);
|
|
91
|
-
|
|
92
|
-
// A real 404, so misses are not cached as successes now that this route is
|
|
93
|
-
// statically renderable. An outage is deliberately not a 404 — that would
|
|
94
|
-
// deindex published pages over a transient blip.
|
|
95
|
-
if (result.status === "missing") {
|
|
96
|
-
notFound();
|
|
97
|
-
}
|
|
98
|
-
if (result.status === "unavailable") {
|
|
99
|
-
return <ContentUnavailable />;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const data = result.data;
|
|
103
|
-
|
|
104
|
-
const [routeTemplateKeys, ccrQueryFetchers] = await Promise.all([
|
|
105
|
-
loadRouteTemplateKeys(),
|
|
106
|
-
getCcrQueryFetchers(),
|
|
107
|
-
]);
|
|
108
|
-
const builtinFetchers = [...REMOTE_DATASOURCE_FETCHERS, ...ccrQueryFetchers];
|
|
109
|
-
|
|
110
|
-
const referencedDatasourceIds = extractReferencedDatasourceIds(data);
|
|
111
|
-
const context = await loadRemoteDatasourceContext({
|
|
112
|
-
fetchImpl: fetch,
|
|
113
|
-
pagePath: path,
|
|
114
|
-
routeTemplateKeys,
|
|
115
|
-
builtinFetchers,
|
|
116
|
-
referencedDatasourceIds,
|
|
117
|
-
});
|
|
118
|
-
const resolvedData = await resolveDataTemplates(data, context);
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<Client
|
|
122
|
-
data={resolvedData}
|
|
123
|
-
pageMetadata={{
|
|
124
|
-
route: path,
|
|
125
|
-
documentName: data.root.props?.title as string | undefined,
|
|
126
|
-
pageType: "page",
|
|
127
|
-
}}
|
|
128
|
-
/>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
21
|
+
export const generateStaticParams = published.generateStaticParams;
|
|
22
|
+
export const generateMetadata = published.generateMetadata;
|
|
23
|
+
export default published.Page;
|
|
@@ -31,42 +31,12 @@ import "@pantheon-systems/puck-css/pds/styles.css";
|
|
|
31
31
|
|
|
32
32
|
import { ChatbotFlagProvider } from "../../../../components/ChatbotFlagProvider";
|
|
33
33
|
import { P1Lockup } from "../../../../components/p1-lockup";
|
|
34
|
+
import styles from "../../../../components/welcome-block.module.css";
|
|
34
35
|
import config from "../../../../puck.config";
|
|
35
36
|
import { shouldShowChatbot, CHATBOT_FLAG_KEY } from "../../../../lib/chatbot-flag/feature-gate";
|
|
36
37
|
import { createGenerateWithAIHandler } from "../../../../lib/chatbot-flag/ai-generate";
|
|
37
38
|
import { getDraftRequestChannel } from "../../../../lib/chatbot-flag/draft-request-channel";
|
|
38
39
|
|
|
39
|
-
const DEFAULT_PAGE_DATA = {
|
|
40
|
-
root: { props: { title: "New page" } },
|
|
41
|
-
content: [],
|
|
42
|
-
zones: {},
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const DEFAULT_ROOT_PAGE_DATA = {
|
|
46
|
-
root: { props: { title: "Welcome | P1 site" } },
|
|
47
|
-
content: [
|
|
48
|
-
{
|
|
49
|
-
type: "P1WelcomeBlock",
|
|
50
|
-
props: {
|
|
51
|
-
id: "seed-welcome",
|
|
52
|
-
heading: "Welcome to your new Pantheon P1 Site.",
|
|
53
|
-
description: "You just created this new site from Pantheon P1 starter kit, congrats! You'll need a Pantheon P1 user account to edit it and create new pages.",
|
|
54
|
-
ctaLabel: "Sign-in to P1",
|
|
55
|
-
ctaHref: "/p1",
|
|
56
|
-
footnote: "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
|
|
57
|
-
loggedInHeading: "Welcome to your new Pantheon P1 Site.",
|
|
58
|
-
loggedInDescription: "You just created this new site from Pantheon P1 starter kit, congrats! Start editing this page or visit the P1 dashboard to manage your site.",
|
|
59
|
-
loggedInCtaLabel: "Edit this page with P1 Visual Editor",
|
|
60
|
-
loggedInCtaHref: "/p1",
|
|
61
|
-
loggedInSecondaryLabel: "Go to P1 Dashboard",
|
|
62
|
-
loggedInFootnote: "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
|
|
63
|
-
showLogo: true,
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
zones: {},
|
|
68
|
-
};
|
|
69
|
-
|
|
70
40
|
let p1Config: ReturnType<typeof createNextConfig> | null = null;
|
|
71
41
|
let p1ConfigError: string | null = null;
|
|
72
42
|
|
|
@@ -84,33 +54,31 @@ function P1SignInPage() {
|
|
|
84
54
|
const { login, isLoading, error } = useP1Auth();
|
|
85
55
|
|
|
86
56
|
return (
|
|
87
|
-
<div className=
|
|
88
|
-
<
|
|
57
|
+
<div className={styles.surface}>
|
|
58
|
+
<div className={styles.inner}>
|
|
59
|
+
<P1Lockup />
|
|
89
60
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
61
|
+
<h1 className={styles.heading}>
|
|
62
|
+
Your Collaborative Website Management Workspace.
|
|
63
|
+
</h1>
|
|
64
|
+
<p className={styles.description}>
|
|
65
|
+
Log in to your Pantheon P1 account to edit your P1 powered website.
|
|
66
|
+
If you don't have yet a Pantheon P1 account, contact us{" "}
|
|
67
|
+
<a href="https://pantheon.io/contact-us" className={styles.link}>here</a>.
|
|
68
|
+
</p>
|
|
98
69
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
70
|
+
<div className={styles.actions}>
|
|
71
|
+
<button
|
|
72
|
+
className={styles.button}
|
|
73
|
+
onClick={() => void login()}
|
|
74
|
+
disabled={isLoading}
|
|
75
|
+
>
|
|
76
|
+
{isLoading ? "Signing in..." : "Continue"}
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
108
79
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
{error}
|
|
112
|
-
</p>
|
|
113
|
-
)}
|
|
80
|
+
{error && <p className={styles.error}>{error}</p>}
|
|
81
|
+
</div>
|
|
114
82
|
</div>
|
|
115
83
|
);
|
|
116
84
|
}
|
|
@@ -208,7 +176,6 @@ function RoleSwitcher({
|
|
|
208
176
|
|
|
209
177
|
function EditorContent({ path }: { path: string }) {
|
|
210
178
|
const router = useRouter();
|
|
211
|
-
const { getToken } = useP1Auth();
|
|
212
179
|
const { data: editorCtx } = useEditorContext(path);
|
|
213
180
|
const {
|
|
214
181
|
context: remoteDatasourceContext,
|
|
@@ -258,27 +225,10 @@ function EditorContent({ path }: { path: string }) {
|
|
|
258
225
|
[router],
|
|
259
226
|
);
|
|
260
227
|
|
|
261
|
-
const handleDocumentNotFound = useCallback(
|
|
262
|
-
async (docPath: string, _error: Error) => {
|
|
263
|
-
const initialData = docPath === "/" ? DEFAULT_ROOT_PAGE_DATA : DEFAULT_PAGE_DATA;
|
|
264
|
-
const token = await getToken();
|
|
265
|
-
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
266
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
267
|
-
const res = await fetch("/p1/api/structure/page", {
|
|
268
|
-
method: "POST",
|
|
269
|
-
headers,
|
|
270
|
-
body: JSON.stringify({ path: docPath, initialData }),
|
|
271
|
-
});
|
|
272
|
-
return res.ok;
|
|
273
|
-
},
|
|
274
|
-
[getToken],
|
|
275
|
-
);
|
|
276
|
-
|
|
277
228
|
const { loading, reloading, hasContent, error, puckKey, puckProps } = useP1Editor({
|
|
278
229
|
documentPath: path,
|
|
279
230
|
puckConfig: editorConfig,
|
|
280
231
|
additionalPlugins,
|
|
281
|
-
onDocumentNotFound: handleDocumentNotFound,
|
|
282
232
|
pluginOptions: {
|
|
283
233
|
onDocumentSelect: handleDocumentSelect,
|
|
284
234
|
onGenerateWithAI: createGenerateWithAIHandler(draftRequests, chatbotEnabled),
|
package/template/app/page.tsx
CHANGED
|
@@ -1,77 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
loadPublishedPage,
|
|
8
|
-
loadRouteTemplateKeys,
|
|
9
|
-
} from "@pantheon-systems/p1-next-sdk/server";
|
|
10
|
-
import type { Metadata } from "next";
|
|
11
|
-
import { WelcomeBlockRender } from "../components/puck/welcome-block-render";
|
|
12
|
-
import { resolvePageMetadata } from "../lib/page-seo";
|
|
13
|
-
import { REMOTE_DATASOURCE_FETCHERS } from "../lib/remote-datasource-fetchers";
|
|
14
|
-
import { Client } from "./[...puckPath]/client";
|
|
1
|
+
/**
|
|
2
|
+
* Home route. The pipeline lives in the SDK — see createPublishedPage in
|
|
3
|
+
* app/published-pages.tsx for what this route is made of.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { published } from "./published-pages";
|
|
15
7
|
|
|
16
8
|
/**
|
|
17
9
|
* Backstop only: publishing calls revalidatePath("/"), so the home page
|
|
18
10
|
* normally refreshes the moment its content changes. This bounds how long an
|
|
19
11
|
* edit made outside that path can stay stale.
|
|
12
|
+
*
|
|
13
|
+
* Must stay a literal here: Next.js statically analyzes segment-config exports,
|
|
14
|
+
* so a value re-exported from the factory would go undetected.
|
|
20
15
|
*/
|
|
21
16
|
export const revalidate = 300;
|
|
22
17
|
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
if (result.status !== "ok") {
|
|
26
|
-
return { title: "P1 Starter Kit" };
|
|
27
|
-
}
|
|
28
|
-
return resolvePageMetadata({ pageData: result.data, path: "/" });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export default async function HomePage() {
|
|
32
|
-
const result = await loadPublishedPage("/");
|
|
33
|
-
|
|
34
|
-
// Unlike the catch-all, "/" never 404s: it is a single fixed URL rather than
|
|
35
|
-
// an unbounded crawler surface, and the welcome block is the correct state for
|
|
36
|
-
// a freshly scaffolded site — including one with no backend configured yet.
|
|
37
|
-
if (result.status === "ok") {
|
|
38
|
-
const data = result.data;
|
|
39
|
-
const routeTemplateKeys = await loadRouteTemplateKeys();
|
|
40
|
-
const referencedDatasourceIds = extractReferencedDatasourceIds(data);
|
|
41
|
-
const context = await loadRemoteDatasourceContext({
|
|
42
|
-
fetchImpl: fetch,
|
|
43
|
-
pagePath: "/",
|
|
44
|
-
routeTemplateKeys,
|
|
45
|
-
builtinFetchers: REMOTE_DATASOURCE_FETCHERS,
|
|
46
|
-
referencedDatasourceIds,
|
|
47
|
-
});
|
|
48
|
-
const resolvedData = await resolveDataTemplates(data, context);
|
|
49
|
-
return (
|
|
50
|
-
<Client
|
|
51
|
-
data={resolvedData}
|
|
52
|
-
pageMetadata={{
|
|
53
|
-
route: "/",
|
|
54
|
-
documentName: data.root.props?.title as string | undefined,
|
|
55
|
-
pageType: "page",
|
|
56
|
-
}}
|
|
57
|
-
/>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<WelcomeBlockRender
|
|
63
|
-
heading="Welcome to your new Pantheon P1 Site."
|
|
64
|
-
description="You just created this new site from Pantheon P1 starter kit, congrats! You'll need a Pantheon P1 user account to edit it and create new pages."
|
|
65
|
-
ctaLabel="Sign-in to P1"
|
|
66
|
-
ctaHref="/p1"
|
|
67
|
-
footnote="Visit [P1 documentation](https://docs.pantheon.io) for more information."
|
|
68
|
-
loggedInHeading="Welcome to your new Pantheon P1 Site."
|
|
69
|
-
loggedInDescription="You just created this new site from Pantheon P1 starter kit, congrats! Start editing this page or visit the P1 dashboard to manage your site."
|
|
70
|
-
loggedInCtaLabel="Edit this page with P1 Visual Editor"
|
|
71
|
-
loggedInCtaHref="/p1"
|
|
72
|
-
loggedInSecondaryLabel="Go to P1 Dashboard"
|
|
73
|
-
loggedInFootnote="Visit [P1 documentation](https://docs.pantheon.io) for more information."
|
|
74
|
-
showLogo={true}
|
|
75
|
-
/>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
18
|
+
export const generateMetadata = published.generateHomeMetadata;
|
|
19
|
+
export default published.HomePage;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app's published-page routes, built once and shared by both route files.
|
|
3
|
+
*
|
|
4
|
+
* One instance on purpose: the factory memoizes per-request work (the CCR query
|
|
5
|
+
* fetchers), and "/" and the catch-all should share that rather than each
|
|
6
|
+
* building their own.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createPublishedPage } from "@pantheon-systems/p1-next-sdk/server";
|
|
10
|
+
import { REMOTE_DATASOURCE_FETCHERS } from "../lib/remote-datasource-fetchers";
|
|
11
|
+
import { ContentUnavailable } from "../components/content-unavailable";
|
|
12
|
+
import { resolvePageMetadata } from "../lib/page-seo";
|
|
13
|
+
import { Client } from "./[...puckPath]/client";
|
|
14
|
+
import { WelcomeBlock } from "./welcome-block";
|
|
15
|
+
|
|
16
|
+
export const published = createPublishedPage({
|
|
17
|
+
Client,
|
|
18
|
+
Unavailable: ContentUnavailable,
|
|
19
|
+
Fallback: WelcomeBlock,
|
|
20
|
+
fetchers: REMOTE_DATASOURCE_FETCHERS,
|
|
21
|
+
resolveMetadata: resolvePageMetadata,
|
|
22
|
+
titles: { home: "P1 Starter Kit" },
|
|
23
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The home page's state before any content exists — a freshly scaffolded site,
|
|
3
|
+
* or one with no backend configured yet.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { WelcomeBlockRender } from "../components/puck/welcome-block-render";
|
|
7
|
+
|
|
8
|
+
export function WelcomeBlock() {
|
|
9
|
+
return (
|
|
10
|
+
<WelcomeBlockRender
|
|
11
|
+
heading="Welcome to your new Pantheon P1 Site."
|
|
12
|
+
description="You just created this new site from Pantheon P1 starter kit, congrats! You'll need a Pantheon P1 user account to edit it and create new pages."
|
|
13
|
+
ctaLabel="Sign-in to P1"
|
|
14
|
+
ctaHref="/p1"
|
|
15
|
+
footnote="Visit [P1 documentation](https://docs.pantheon.io) for more information."
|
|
16
|
+
loggedInHeading="Welcome to your new Pantheon P1 Site."
|
|
17
|
+
loggedInDescription="You just created this new site from Pantheon P1 starter kit, congrats! Start editing this page or visit the P1 dashboard to manage your site."
|
|
18
|
+
loggedInCtaLabel="Edit this page with P1 Visual Editor"
|
|
19
|
+
loggedInCtaHref="/p1"
|
|
20
|
+
loggedInSecondaryLabel="Go to P1 Dashboard"
|
|
21
|
+
loggedInFootnote="Visit [P1 documentation](https://docs.pantheon.io) for more information."
|
|
22
|
+
showLogo={true}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -4,6 +4,8 @@ import React from "react";
|
|
|
4
4
|
import { LDProvider } from "launchdarkly-react-client-sdk";
|
|
5
5
|
import { useP1Auth } from "@pantheon-systems/puck-css";
|
|
6
6
|
|
|
7
|
+
import { buildFlagContext } from "../lib/chatbot-flag/flag-context";
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Wraps the editor with a LaunchDarkly client-side provider so the `p1-chatbot`
|
|
9
11
|
* flag can be evaluated at runtime. The client-side ID is public by design.
|
|
@@ -28,14 +30,7 @@ export function ChatbotFlagProvider({
|
|
|
28
30
|
// re-identify on context change. This provider mounts inside <P1App> (after
|
|
29
31
|
// auth), so the authenticated user is available here; the anonymous fallback
|
|
30
32
|
// only applies if it ever renders pre-auth.
|
|
31
|
-
|
|
32
|
-
// Key on the always-present, stable user id — email is optional on AuthUser, so
|
|
33
|
-
// keying on it would silently drop emailless users into the anonymous branch and
|
|
34
|
-
// lose per-user rollout stickiness. (LaunchDarkly also favors a non-PII key.)
|
|
35
|
-
// Email is kept as a targeting attribute.
|
|
36
|
-
const context = user
|
|
37
|
-
? { kind: "user" as const, key: user.id, email: user.email }
|
|
38
|
-
: { kind: "user" as const, key: "anonymous", anonymous: true };
|
|
33
|
+
const context = buildFlagContext(user);
|
|
39
34
|
|
|
40
35
|
return (
|
|
41
36
|
<LDProvider
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { P1_ASSETS } from "../constants/assets";
|
|
4
|
+
import styles from "./welcome-block.module.css";
|
|
4
5
|
|
|
5
6
|
export function P1Lockup() {
|
|
6
7
|
return (
|
|
7
8
|
<img
|
|
8
9
|
src={P1_ASSETS.LOGO_URL}
|
|
9
10
|
alt="Pantheon P1"
|
|
10
|
-
className=
|
|
11
|
+
className={styles.lockup}
|
|
11
12
|
/>
|
|
12
13
|
);
|
|
13
14
|
}
|
|
@@ -16,8 +16,11 @@ export const imageBlock = {
|
|
|
16
16
|
},
|
|
17
17
|
},
|
|
18
18
|
defaultProps: {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Placeholder art inline as a data URI, so a fresh block loads nothing off
|
|
20
|
+
// the network. Authors replace it with their own image.
|
|
21
|
+
src:
|
|
22
|
+
"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221200%22%20height%3D%22675%22%20viewBox%3D%220%200%201200%20675%22%3E%3Crect%20width%3D%221200%22%20height%3D%22675%22%20fill%3D%22%23f1f1f3%22%2F%3E%3Cpath%20d%3D%22M0%20675L1200%200%22%20stroke%3D%22%23c8c8ce%22%20stroke-width%3D%2284%22%2F%3E%3C%2Fsvg%3E",
|
|
23
|
+
alt: "",
|
|
21
24
|
caption: "",
|
|
22
25
|
loading: "lazy",
|
|
23
26
|
},
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import { P1Lockup } from "../p1-lockup";
|
|
5
|
+
import styles from "../welcome-block.module.css";
|
|
5
6
|
|
|
6
7
|
export interface WelcomeBlockRenderProps {
|
|
7
8
|
heading?: string;
|
|
@@ -30,12 +31,6 @@ const LOGGED_IN_DEFAULTS = {
|
|
|
30
31
|
"Visit [P1 documentation](https://docs.pantheon.io) for more information.",
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
const BTN_PRIMARY =
|
|
34
|
-
"inline-flex items-center justify-center h-12 px-6 gap-2 rounded-full border border-[#1a1a2e] bg-[#1a1a2e] text-white font-['Inter',system-ui,sans-serif] text-lg font-medium leading-none whitespace-nowrap cursor-pointer transition-colors duration-200 hover:bg-[#2d2d44] hover:border-[#2d2d44] focus-visible:outline focus-visible:outline-1 focus-visible:outline-blue-600 focus-visible:outline-offset-1 disabled:opacity-40 disabled:cursor-not-allowed";
|
|
35
|
-
|
|
36
|
-
const BTN_SECONDARY =
|
|
37
|
-
"inline-flex items-center justify-center h-12 px-6 gap-2 rounded-full border border-[#d0d0d8] bg-transparent text-[#1a1a2e] font-['Inter',system-ui,sans-serif] text-lg font-medium leading-none whitespace-nowrap cursor-pointer transition-colors duration-200 hover:bg-[rgba(26,26,46,0.06)] focus-visible:outline focus-visible:outline-1 focus-visible:outline-blue-600 focus-visible:outline-offset-1";
|
|
38
|
-
|
|
39
34
|
export function WelcomeBlockRender(props: WelcomeBlockRenderProps) {
|
|
40
35
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
41
36
|
|
|
@@ -64,47 +59,45 @@ export function WelcomeBlockRender(props: WelcomeBlockRenderProps) {
|
|
|
64
59
|
|
|
65
60
|
const footnoteHtml = (activeFootnote ?? "").replace(
|
|
66
61
|
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
67
|
-
|
|
62
|
+
`<a href="$2" target="_blank" rel="noopener noreferrer" class="${styles.link}">$1</a>`,
|
|
68
63
|
);
|
|
69
64
|
|
|
70
65
|
return (
|
|
71
|
-
<div className=
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
{activeHeading}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
{activeDescription}
|
|
78
|
-
</p>
|
|
79
|
-
<div className="flex gap-3 mt-8 justify-center">
|
|
80
|
-
<button
|
|
81
|
-
className={BTN_PRIMARY}
|
|
82
|
-
onClick={() => {
|
|
83
|
-
if (!isLoggedIn) {
|
|
84
|
-
localStorage.setItem("p1_return_to", window.location.pathname);
|
|
85
|
-
}
|
|
86
|
-
window.location.href = activeCtaHref || "/";
|
|
87
|
-
}}
|
|
88
|
-
>
|
|
89
|
-
{activeCtaLabel}
|
|
90
|
-
</button>
|
|
91
|
-
{secondaryLabel && (
|
|
66
|
+
<div className={styles.surface}>
|
|
67
|
+
<div className={styles.inner}>
|
|
68
|
+
{props.showLogo !== false && <P1Lockup />}
|
|
69
|
+
<h1 className={styles.heading}>{activeHeading}</h1>
|
|
70
|
+
<p className={styles.description}>{activeDescription}</p>
|
|
71
|
+
<div className={styles.actions}>
|
|
92
72
|
<button
|
|
93
|
-
className={
|
|
73
|
+
className={styles.button}
|
|
94
74
|
onClick={() => {
|
|
95
|
-
|
|
75
|
+
if (!isLoggedIn) {
|
|
76
|
+
localStorage.setItem("p1_return_to", window.location.pathname);
|
|
77
|
+
}
|
|
78
|
+
window.location.href = activeCtaHref || "/";
|
|
96
79
|
}}
|
|
97
80
|
>
|
|
98
|
-
{
|
|
81
|
+
{activeCtaLabel}
|
|
99
82
|
</button>
|
|
83
|
+
{secondaryLabel && (
|
|
84
|
+
<button
|
|
85
|
+
className={`${styles.button} ${styles.buttonSecondary}`}
|
|
86
|
+
onClick={() => {
|
|
87
|
+
window.open(LOGGED_IN_DEFAULTS.secondaryHref, "_blank", "noopener,noreferrer");
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{secondaryLabel}
|
|
91
|
+
</button>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
{activeFootnote && (
|
|
95
|
+
<p
|
|
96
|
+
className={styles.footnote}
|
|
97
|
+
dangerouslySetInnerHTML={{ __html: footnoteHtml }}
|
|
98
|
+
/>
|
|
100
99
|
)}
|
|
101
100
|
</div>
|
|
102
|
-
{activeFootnote && (
|
|
103
|
-
<p
|
|
104
|
-
className="mt-12 text-sm text-[#5a5a6e]"
|
|
105
|
-
dangerouslySetInnerHTML={{ __html: footnoteHtml }}
|
|
106
|
-
/>
|
|
107
|
-
)}
|
|
108
101
|
</div>
|
|
109
102
|
);
|
|
110
103
|
}
|