@cmssy/cli 10.9.2 → 11.0.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.
@@ -1,29 +1,53 @@
1
- import { createCmssyPage } from "@cmssy/next/server";
1
+ import { createCmssyPage, CmssyLayoutSlot } from "@cmssy/next/server";
2
2
  import { cmssy } from "@/cmssy.config";
3
3
  import { blocks } from "@/cmssy/blocks";
4
4
  import { CmssyEditor } from "@/cmssy/editor";
5
- import { CmssyLayoutSlot } from "@/cmssy/layout-slot";
5
+ import { EditableLayout } from "@/cmssy/editable-layout";
6
+ import { publishedPaths } from "@/services/pages";
6
7
 
7
8
  type PageProps = {
8
9
  params: Promise<{ path?: string[] }>;
9
10
  searchParams: Promise<Record<string, string | string[] | undefined>>;
10
11
  };
11
12
 
13
+ // Three lines that decide whether this site is cached at all. Without
14
+ // generateStaticParams a catch-all is rendered on demand every time and
15
+ // `revalidate` is ignored - the build prints a blank Revalidate column, which is
16
+ // the only warning you get. `dynamicParams` keeps pages published after the
17
+ // build working: first request renders them, and they cache from then on.
18
+ export const revalidate = 3600;
19
+ export const dynamicParams = true;
20
+
21
+ export function generateStaticParams() {
22
+ return publishedPaths();
23
+ }
24
+
12
25
  const CmssyPage = createCmssyPage(cmssy, blocks, { editor: CmssyEditor });
13
26
 
14
- // The header and the footer are layout blocks, mounted here rather than in
15
- // app/layout.tsx: this route knows its path, and the language prefix in that
16
- // path is what says which language to render them in. Reading it from the
27
+ // The header and footer are layout blocks. They are mounted here rather than in
28
+ // app/layout.tsx because this route knows its path, and the language prefix in
29
+ // that path is what says which language to render them in - reading it from the
17
30
  // request header instead would make every page dynamic.
18
31
  export default async function Page(props: PageProps) {
19
32
  const { path } = await props.params;
33
+ const slot = (position: "header" | "footer") => (
34
+ <CmssyLayoutSlot
35
+ config={cmssy}
36
+ blocks={blocks}
37
+ position={position}
38
+ path={path ?? []}
39
+ editMode={false}
40
+ editable={EditableLayout}
41
+ />
42
+ );
43
+
20
44
  return (
21
45
  <>
22
- <CmssyLayoutSlot position="header" path={path} />
46
+ {slot("header")}
23
47
  <main>
24
48
  <CmssyPage {...props} />
25
49
  </main>
26
- <CmssyLayoutSlot position="footer" path={path} />
50
+ {slot("footer")}
27
51
  </>
28
52
  );
29
53
  }
@@ -1,8 +1,12 @@
1
- import { createCmssyEditPage } from "@cmssy/next/server";
1
+ import {
2
+ createCmssyEditPage,
3
+ CmssyLayoutSlot,
4
+ isCmssyEditMode,
5
+ } from "@cmssy/next/server";
2
6
  import { cmssy } from "@/cmssy.config";
3
7
  import { blocks } from "@/cmssy/blocks";
4
8
  import { CmssyEditor } from "@/cmssy/editor";
5
- import { CmssyLayoutSlot } from "@/cmssy/layout-slot";
9
+ import { EditableLayout } from "@/cmssy/editable-layout";
6
10
 
7
11
  // The route the middleware rewrites a verified editor request onto. The public
8
12
  // pages stay static, and a static page never sees the query string that would
@@ -24,13 +28,31 @@ const CmssyEditPage = createCmssyEditPage(cmssy, blocks, {
24
28
  // header missing here is a header missing in the editor.
25
29
  export default async function EditPage(props: PageProps) {
26
30
  const { path } = await props.params;
31
+
32
+ // Not a hard `true`: this route is reachable directly, and only the proxy's
33
+ // verified rewrite sets the edit header. Hard-coding it would fetch layouts
34
+ // with the draft secret for anyone who typed the URL. Reading a header costs
35
+ // nothing here - the route is force-dynamic either way.
36
+ const editMode = await isCmssyEditMode();
37
+
38
+ const slot = (position: "header" | "footer") => (
39
+ <CmssyLayoutSlot
40
+ config={cmssy}
41
+ blocks={blocks}
42
+ position={position}
43
+ path={path ?? []}
44
+ editMode={editMode}
45
+ editable={EditableLayout}
46
+ />
47
+ );
48
+
27
49
  return (
28
50
  <>
29
- <CmssyLayoutSlot position="header" path={path} />
51
+ {slot("header")}
30
52
  <main>
31
53
  <CmssyEditPage {...props} />
32
54
  </main>
33
- <CmssyLayoutSlot position="footer" path={path} />
55
+ {slot("footer")}
34
56
  </>
35
57
  );
36
58
  }
@@ -0,0 +1,58 @@
1
+ import { createCmssyClient } from "@cmssy/core";
2
+ import { cmssy } from "@/cmssy.config";
3
+
4
+ // Your query, in your repo. The SDK does not mirror the graph - it gives you a
5
+ // client and gets out of the way, so adding a field here is your call and not a
6
+ // release of ours.
7
+ const PUBLISHED_PAGES = `query PublishedPages($workspaceSlug: String!) {
8
+ public {
9
+ page {
10
+ list(workspaceSlug: $workspaceSlug) {
11
+ slug
12
+ publishedAt
13
+ }
14
+ }
15
+ }
16
+ }`;
17
+
18
+ type PublishedPages = {
19
+ public?: {
20
+ page?: { list?: { slug?: string | null; publishedAt?: string | null }[] | null } | null;
21
+ } | null;
22
+ };
23
+
24
+ const client = createCmssyClient({
25
+ apiUrl: cmssy.apiUrl,
26
+ org: cmssy.org,
27
+ workspaceSlug: cmssy.workspaceSlug,
28
+ });
29
+
30
+ /**
31
+ * The paths to prerender, as `generateStaticParams` wants them.
32
+ *
33
+ * A catch-all route with no params generated is served on demand and cached by
34
+ * nothing, whatever `revalidate` says - so this function is what makes the
35
+ * route cacheable at all, not an optimisation on top of it.
36
+ */
37
+ export async function publishedPaths(): Promise<{ path: string[] }[]> {
38
+ const data = await client.query<PublishedPages>(
39
+ PUBLISHED_PAGES,
40
+ { workspaceSlug: cmssy.workspaceSlug },
41
+ {
42
+ // `public` routes through the org-scoped delivery path. Without it the
43
+ // request lands on the base endpoint, where an unauthenticated workspace
44
+ // lookup is by slug alone - across every org.
45
+ public: true,
46
+ // A build reads this once. A single 429 from the delivery API would fail
47
+ // the whole deploy, so retry - it is a read, nothing to double-apply.
48
+ retry: {},
49
+ },
50
+ );
51
+
52
+ const pages = data?.public?.page?.list ?? [];
53
+ return pages
54
+ .filter((page) => page.publishedAt)
55
+ .map((page) => ({
56
+ path: (page.slug ?? "").split("/").filter(Boolean),
57
+ }));
58
+ }
package/dist/index.js CHANGED
@@ -161,14 +161,14 @@ var FRAMEWORKS = [
161
161
  label: "Next.js",
162
162
  createCommand: "npx create-next-app@latest",
163
163
  detect: ["next"],
164
- dependencies: ["@cmssy/next", "@cmssy/react"],
164
+ dependencies: ["@cmssy/next", "@cmssy/react", "@cmssy/core"],
165
165
  files: [
166
166
  "cmssy.config.ts",
167
167
  "proxy.ts",
168
+ "services/pages.ts",
168
169
  "cmssy/blocks.ts",
169
170
  "cmssy/editor.tsx",
170
171
  "cmssy/editable-layout.tsx",
171
- "cmssy/layout-slot.tsx",
172
172
  "blocks/hero/block.ts",
173
173
  "blocks/hero/Hero.tsx",
174
174
  "app/[[...path]]/page.tsx",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/cli",
3
- "version": "10.9.2",
3
+ "version": "11.0.0",
4
4
  "description": "The cmssy CLI: `cmssy init` wires cmssy into an existing Next.js, Astro or React Router app; `cmssy link` connects it to a workspace and verifies the editor wiring.",
5
5
  "keywords": [
6
6
  "cmssy",
@@ -27,7 +27,7 @@
27
27
  "assets"
28
28
  ],
29
29
  "dependencies": {
30
- "@cmssy/core": "10.9.2"
30
+ "@cmssy/core": "11.0.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^20.0.0",
@@ -1,172 +0,0 @@
1
- import { cache } from "react";
2
- import {
3
- CmssyServerLayout,
4
- graphqlRequest,
5
- resolveEditorLayoutBlockData,
6
- } from "@cmssy/react";
7
- import type { CmssyLayoutGroup } from "@cmssy/react";
8
- import { resolveEditorOrigin } from "@cmssy/next";
9
- import { isCmssyEditMode } from "@cmssy/next/server";
10
- import { cmssy } from "@/cmssy.config";
11
- import { blocks } from "@/cmssy/blocks";
12
- import { EditableLayout } from "@/cmssy/editable-layout";
13
-
14
- // The header and the footer are layout blocks: content, not markup. Fetching
15
- // and rendering them is your app's, so both queries live here.
16
- const LAYOUTS_QUERY = `query PublicPageLayouts($workspaceSlug: String!, $pageSlug: String!, $previewSecret: String) {
17
- public {
18
- page {
19
- layouts(workspaceSlug: $workspaceSlug, pageSlug: $pageSlug, previewSecret: $previewSecret) {
20
- position
21
- blocks { id type content style advanced order isActive }
22
- settings { desktopWidth mobileBehavior }
23
- }
24
- }
25
- }
26
- }`;
27
-
28
- const SITE_CONFIG_QUERY = `query PublicSiteConfig($workspaceSlug: String!) {
29
- public {
30
- siteConfig(workspaceSlug: $workspaceSlug) {
31
- defaultLanguage
32
- enabledLanguages
33
- }
34
- }
35
- }`;
36
-
37
- // Both slots render on every page; `cache` makes that one request each.
38
- const fetchLayouts = cache(
39
- async (pageSlug: string, previewSecret: string | null) => {
40
- try {
41
- const data = await graphqlRequest<{
42
- public?: { page?: { layouts?: CmssyLayoutGroup[] | null } | null } | null;
43
- }>(
44
- cmssy,
45
- LAYOUTS_QUERY,
46
- { workspaceSlug: cmssy.workspaceSlug, pageSlug, previewSecret },
47
- { public: true, retry: {} },
48
- "page layouts",
49
- );
50
- return data.public?.page?.layouts ?? [];
51
- } catch (error) {
52
- console.error("[cmssy] page layouts could not be fetched", error);
53
- return [];
54
- }
55
- },
56
- );
57
-
58
- const fetchLocales = cache(async () => {
59
- try {
60
- const data = await graphqlRequest<{
61
- public?: {
62
- siteConfig?: {
63
- defaultLanguage: string | null;
64
- enabledLanguages: string[] | null;
65
- } | null;
66
- } | null;
67
- }>(
68
- cmssy,
69
- SITE_CONFIG_QUERY,
70
- { workspaceSlug: cmssy.workspaceSlug },
71
- { public: true, retry: {} },
72
- "site config",
73
- );
74
- const config = data.public?.siteConfig;
75
- // The languages are the workspace's, never this repo's.
76
- const defaultLocale = config?.defaultLanguage ?? "";
77
- const enabled = config?.enabledLanguages ?? [];
78
- return {
79
- defaultLocale,
80
- locales: enabled.length > 0 ? enabled : defaultLocale ? [defaultLocale] : [],
81
- };
82
- } catch (error) {
83
- console.error("[cmssy] site config could not be fetched", error);
84
- return { defaultLocale: "", locales: [] as string[] };
85
- }
86
- });
87
-
88
- export interface CmssyLayoutSlotProps {
89
- position: "header" | "footer";
90
- /**
91
- * The catch-all segments of the route rendering this slot, as routed. The
92
- * language prefix in them IS the language - reading it here keeps the route
93
- * statically renderable, where reading the request header would force every
94
- * page dynamic and kill ISR.
95
- */
96
- path?: string[];
97
- /** The page whose layout to render. Defaults to the site-wide header/footer. */
98
- page?: string;
99
- }
100
-
101
- /**
102
- * The site-wide header and footer, rendered the way each mode needs them:
103
- *
104
- * - published traffic: server-rendered layout blocks, no client cost;
105
- * - the editor: the same blocks through the edit bridge, fetched with the
106
- * preview secret, so what you see is the draft you are editing.
107
- *
108
- * Getting this wrong is invisible - the site looks right while the editor shows
109
- * a header it can select but not fill, or the published version of one.
110
- */
111
- export async function CmssyLayoutSlot({
112
- position,
113
- path,
114
- page = "/",
115
- }: CmssyLayoutSlotProps) {
116
- const [editMode, { defaultLocale, locales }] = await Promise.all([
117
- isCmssyEditMode(),
118
- fetchLocales(),
119
- ]);
120
-
121
- const [first] = path ?? [];
122
- const locale =
123
- first && first !== defaultLocale && locales.includes(first)
124
- ? first
125
- : defaultLocale;
126
-
127
- const groups = await fetchLayouts(
128
- page,
129
- editMode ? cmssy.draftSecret : null,
130
- );
131
-
132
- if (!editMode) {
133
- return (
134
- <CmssyServerLayout
135
- groups={groups}
136
- blocks={blocks}
137
- position={position}
138
- locale={locale}
139
- defaultLocale={defaultLocale}
140
- enabledLocales={locales}
141
- config={cmssy}
142
- />
143
- );
144
- }
145
-
146
- const origin = resolveEditorOrigin(cmssy.editorOrigin);
147
- // The editor canvas renders stored content, and for a relation field that is
148
- // raw ids - so the server-resolved content is handed over with it.
149
- const editorData = await resolveEditorLayoutBlockData({
150
- groups,
151
- blocks,
152
- position,
153
- locale,
154
- defaultLocale,
155
- enabledLocales: locales,
156
- isPreview: true,
157
- config: cmssy,
158
- });
159
-
160
- return (
161
- <EditableLayout
162
- groups={groups}
163
- position={position}
164
- locale={locale}
165
- defaultLocale={defaultLocale}
166
- enabledLocales={locales}
167
- edit={{ editorOrigin: Array.isArray(origin) ? origin[0] : origin }}
168
- data={editorData.data}
169
- resolvedContent={editorData.content}
170
- />
171
- );
172
- }