@goodfellow-cms/next 0.7.0 → 0.8.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.
@@ -56,8 +56,10 @@ import {
56
56
  writeCalendarFiles,
57
57
  writeContentApi,
58
58
  writeEditorPolicy,
59
+ writeExportHeaders,
59
60
  writeFeedFiles,
60
61
  writeFontFiles,
62
+ writeImageFiles,
61
63
  writePageLanguages,
62
64
  writePluginFiles,
63
65
  writeRedirectFiles,
@@ -339,7 +341,10 @@ if (command === "finish") {
339
341
  `Cloudflare Pages reads only the first ${CLOUDFLARE_REDIRECTS_LIMIT.toLocaleString("en")} lines of _redirects, so ${redirects.cloudflareLeft} old address${redirects.cloudflareLeft === 1 ? " is" : "es are"} left to the pages that send visitors on.`,
340
342
  );
341
343
  }
342
- for (const line of buildNotes(await writeFontFiles(dir))) console.log(line);
344
+ // Cloudflare Pages' header rules, after the site's own public/_headers.
345
+ await writeExportHeaders(dir);
346
+ const images = await writeImageFiles(dir);
347
+ for (const line of buildNotes({ ...(await writeFontFiles(dir)), ...(images && { images }) })) console.log(line);
343
348
  // Plugins' admin frames and build steps, once Goodfellow has written the rest, before the index and the guards.
344
349
  const plugins = await writePluginFiles(dir);
345
350
  if (plugins.frames.length > 0) console.log(`Wrote the admin frames of ${plugins.frames.join(", ")}`);
package/dist/admin.js CHANGED
@@ -6,7 +6,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { Admin, OutdatedAdminPage, reloadFrames } from "@goodfellow-cms/admin";
7
7
  import "@goodfellow-cms/admin/styles.css";
8
8
  //#region src/generated/theme-css.ts
9
- var theme_css_default = "/*\n * Maps the site theme (CSS variables written from content/site.json) onto\n * Tailwind, so classes like bg-primary, text-muted-foreground, font-heading and\n * rounded-lg follow the theme. Import after \"tailwindcss\".\n */\n/*\n * Blocks' dark: classes apply only where the site has dark mode\n * (Site Settings), which gives its pages <meta name=\"color-scheme\"\n * content=\"light dark\">: when the visitor's computer is set to dark, or when\n * the admin panel's preview shows the site dark. Light-only sites never\n * use them, even on a computer set to dark.\n */\n@custom-variant dark {\n @media (prefers-color-scheme: dark) {\n &:where(\n :root:has(meta[name=\"color-scheme\"][content~=\"dark\"]):not([data-gf-color-scheme=\"light\"]),\n :root:has(meta[name=\"color-scheme\"][content~=\"dark\"]):not([data-gf-color-scheme=\"light\"]) *\n ) {\n @slot;\n }\n }\n &:where([data-gf-color-scheme=\"dark\"], [data-gf-color-scheme=\"dark\"] *) {\n @slot;\n }\n}\n\n@theme inline {\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-primary: var(--primary);\n --color-primary-foreground: var(--primary-foreground);\n --color-secondary: var(--secondary);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-muted: var(--muted);\n --color-muted-foreground: var(--muted-foreground);\n --color-accent: var(--accent);\n --color-accent-foreground: var(--accent-foreground);\n --color-border: var(--border);\n /* The rest of shadcn/ui's colors, for blocks built with it, follow the ones above unless custom CSS sets them. */\n --color-card: var(--card, var(--background));\n --color-card-foreground: var(--card-foreground, var(--foreground));\n --color-popover: var(--popover, var(--background));\n --color-popover-foreground: var(--popover-foreground, var(--foreground));\n --color-destructive: var(--destructive, #dc2626);\n --color-input: var(--input, var(--border));\n --color-ring: var(--ring, var(--primary));\n --font-sans: var(--gf-font-body);\n --font-heading: var(--gf-font-heading);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n}\n\n@layer base {\n *,\n ::before,\n ::after {\n border-color: var(--color-border);\n }\n\n body {\n background-color: var(--color-background);\n color: var(--color-foreground);\n /* Keeps the footer at the bottom of short pages. */\n display: flex;\n flex-direction: column;\n min-height: 100dvh;\n }\n\n .gf-main {\n flex: 1;\n }\n\n /* The credits of the photos on a page, which their licenses ask for. */\n .gf-credits {\n max-width: 72rem;\n margin: 0 auto;\n padding: 1.5rem 1rem;\n font-size: 0.75rem;\n line-height: 1.5;\n color: var(--color-muted-foreground);\n }\n\n .gf-credits a {\n text-decoration: underline;\n }\n\n h1,\n h2,\n h3,\n h4,\n h5,\n h6 {\n font-family: var(--font-heading);\n }\n}\n";
9
+ var theme_css_default = "/*\n * Maps the site theme (CSS variables written from content/site.json) onto\n * Tailwind, so classes like bg-primary, text-muted-foreground, font-heading and\n * rounded-lg follow the theme. Import after \"tailwindcss\".\n */\n/*\n * Blocks' dark: classes apply only where the site has dark mode\n * (Site Settings), which gives its pages <meta name=\"color-scheme\"\n * content=\"light dark\">: when the visitor's computer is set to dark, or when\n * the admin panel's preview shows the site dark. Light-only sites never\n * use them, even on a computer set to dark.\n */\n@custom-variant dark {\n @media (prefers-color-scheme: dark) {\n &:where(\n :root:has(meta[name=\"color-scheme\"][content~=\"dark\"]):not([data-gf-color-scheme=\"light\"]),\n :root:has(meta[name=\"color-scheme\"][content~=\"dark\"]):not([data-gf-color-scheme=\"light\"]) *\n ) {\n @slot;\n }\n }\n &:where([data-gf-color-scheme=\"dark\"], [data-gf-color-scheme=\"dark\"] *) {\n @slot;\n }\n}\n\n@theme inline {\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-primary: var(--primary);\n --color-primary-foreground: var(--primary-foreground);\n --color-secondary: var(--secondary);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-muted: var(--muted);\n --color-muted-foreground: var(--muted-foreground);\n --color-accent: var(--accent);\n --color-accent-foreground: var(--accent-foreground);\n --color-border: var(--border);\n /* The rest of shadcn/ui's colors, for blocks built with it, follow the ones above unless custom CSS sets them. */\n --color-card: var(--card, var(--background));\n --color-card-foreground: var(--card-foreground, var(--foreground));\n --color-popover: var(--popover, var(--background));\n --color-popover-foreground: var(--popover-foreground, var(--foreground));\n --color-destructive: var(--destructive, #dc2626);\n --color-input: var(--input, var(--border));\n --color-ring: var(--ring, var(--primary));\n --font-sans: var(--gf-font-body);\n --font-heading: var(--gf-font-heading);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n}\n\n@layer base {\n *,\n ::before,\n ::after {\n border-color: var(--color-border);\n }\n\n body {\n background-color: var(--color-background);\n color: var(--color-foreground);\n /* Keeps the footer at the bottom of short pages. */\n display: flex;\n flex-direction: column;\n min-height: 100dvh;\n }\n\n .gf-main {\n flex: 1;\n }\n\n /* The credits of the photos on a page, which their licenses ask for. */\n .gf-credits {\n max-width: 72rem;\n margin: 0 auto;\n padding: 1.5rem 1rem;\n font-size: 0.75rem;\n line-height: 1.5;\n color: var(--color-muted-foreground);\n }\n\n .gf-credits a {\n text-decoration: underline;\n }\n\n /*\n * On a site that asks visitors before loading other sites' content: the\n * notice, and what's shown in a held-back player's place.\n */\n .gf-consent {\n position: fixed;\n inset-inline: 1rem;\n bottom: 1rem;\n z-index: 50;\n max-width: 40rem;\n margin-inline: auto;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n padding: 1rem;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-lg);\n background-color: var(--color-background);\n color: var(--color-foreground);\n box-shadow: 0 10px 30px rgb(0 0 0 / 0.15);\n font-size: 0.875rem;\n line-height: 1.5;\n }\n\n .gf-consent a {\n text-decoration: underline;\n }\n\n .gf-consent-buttons {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.5rem;\n }\n\n .gf-consent button,\n .gf-consent-placeholder button {\n padding: 0.5rem 1rem;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n font-weight: 500;\n cursor: pointer;\n }\n\n .gf-consent [data-gf-consent-accept],\n .gf-consent-placeholder button {\n border-color: var(--color-primary);\n background-color: var(--color-primary);\n color: var(--color-primary-foreground);\n }\n\n .gf-consent-placeholder {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 0.75rem;\n padding: 1rem;\n border-radius: inherit;\n background-color: var(--color-muted);\n color: var(--color-foreground);\n font-size: 0.875rem;\n text-align: center;\n }\n\n /* In a form, beside its other fields, rather than over a player. */\n .gf-consent-placeholder.gf-consent-inline {\n position: static;\n align-items: flex-start;\n text-align: start;\n border-radius: var(--radius-md);\n }\n\n .gf-consent-box {\n position: relative;\n }\n\n /* A choice for each kind of content, which Choose shows. */\n .gf-consent-choices {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n margin: 0;\n padding: 0;\n border: 0;\n }\n\n .gf-consent-choices label {\n display: flex;\n align-items: flex-start;\n gap: 0.5rem;\n }\n\n .gf-consent-choices input {\n margin-top: 0.25rem;\n accent-color: var(--color-primary);\n }\n\n /* Hidden until the consent script shows them, whatever else styles them. */\n .gf-consent[hidden],\n .gf-consent-placeholder[hidden],\n .gf-consent-choices[hidden],\n .gf-consent button[hidden] {\n display: none;\n }\n\n h1,\n h2,\n h3,\n h4,\n h5,\n h6 {\n font-family: var(--font-heading);\n }\n}\n";
10
10
  //#endregion
11
11
  //#region src/admin.tsx
12
12
  /**
@@ -10,11 +10,21 @@ export declare function NextImage({ src, alt, width, height, loading, ...props }
10
10
  * Marks the current page's links in the site's menus (`data-current`, and
11
11
  * `aria-current` for the page itself), as Menu does when it knows the page. The
12
12
  * layout's header and footer are shared by every page, so they can't know it;
13
- * this runs in the browser after every navigation instead.
13
+ * this runs in the browser after every navigation instead, and closes a menu
14
+ * button's links, which would otherwise stay open on the new page.
14
15
  */
15
16
  export declare function CurrentMenuLinks({ basePath }: {
16
17
  basePath?: string;
17
18
  }): null;
19
+ /**
20
+ * Opens the Search box's results with Next.js's navigation, and loads them
21
+ * ahead when they're pointed at or reached with the keyboard, as `next/link`
22
+ * does. The Search box writes its results itself (`searchScript()`), as plain
23
+ * links, so pages built with `goodfellow build` need no React for them.
24
+ */
25
+ export declare function SearchResultLinks({ basePath }: {
26
+ basePath?: string;
27
+ }): null;
18
28
  /**
19
29
  * On a site in several languages, says the page's language on `<html>`, which
20
30
  * the root layout shared by every page can't. `goodfellow-next finish` writes
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { CurrentPathProvider } from "@goodfellow-cms/react";
3
- import { usePathname } from "next/navigation";
3
+ import { usePathname, useRouter } from "next/navigation";
4
4
  import { useEffect } from "react";
5
5
  import Image from "next/image";
6
6
  import Link from "next/link";
@@ -29,7 +29,8 @@ function trimSlash(path) {
29
29
  * Marks the current page's links in the site's menus (`data-current`, and
30
30
  * `aria-current` for the page itself), as Menu does when it knows the page. The
31
31
  * layout's header and footer are shared by every page, so they can't know it;
32
- * this runs in the browser after every navigation instead.
32
+ * this runs in the browser after every navigation instead, and closes a menu
33
+ * button's links, which would otherwise stay open on the new page.
33
34
  */
34
35
  function CurrentMenuLinks({ basePath = "" }) {
35
36
  const pathname = usePathname();
@@ -45,10 +46,52 @@ function CurrentMenuLinks({ basePath = "" }) {
45
46
  if (exact) link.setAttribute("aria-current", "page");
46
47
  else link.removeAttribute("aria-current");
47
48
  }
49
+ for (const panel of document.querySelectorAll("[data-gf-menu] details[open]")) panel.open = false;
48
50
  }, [pathname, basePath]);
49
51
  return null;
50
52
  }
51
53
  /**
54
+ * Opens the Search box's results with Next.js's navigation, and loads them
55
+ * ahead when they're pointed at or reached with the keyboard, as `next/link`
56
+ * does. The Search box writes its results itself (`searchScript()`), as plain
57
+ * links, so pages built with `goodfellow build` need no React for them.
58
+ */
59
+ function SearchResultLinks({ basePath = "" }) {
60
+ const router = useRouter();
61
+ useEffect(() => {
62
+ const prefix = trimSlash(basePath) === "/" ? "" : trimSlash(basePath);
63
+ /** A result's address within the site, without the base path, which Next.js's router adds. */
64
+ const target = (event) => {
65
+ const link = event.target?.closest?.("[data-gf-search-box] a[href]");
66
+ if (!(link instanceof HTMLAnchorElement) || link.target && link.target !== "_self") return void 0;
67
+ const url = new URL(link.href, location.href);
68
+ if (url.origin !== location.origin) return void 0;
69
+ if (prefix && url.pathname !== prefix && !url.pathname.startsWith(`${prefix}/`)) return void 0;
70
+ return `${url.pathname.slice(prefix.length) || "/"}${url.search}${url.hash}`;
71
+ };
72
+ const open = (event) => {
73
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
74
+ const to = target(event);
75
+ if (!to) return;
76
+ event.preventDefault();
77
+ router.push(to);
78
+ };
79
+ const prefetch = (event) => {
80
+ const to = target(event);
81
+ if (to) router.prefetch(to);
82
+ };
83
+ document.addEventListener("click", open);
84
+ document.addEventListener("pointerover", prefetch);
85
+ document.addEventListener("focusin", prefetch);
86
+ return () => {
87
+ document.removeEventListener("click", open);
88
+ document.removeEventListener("pointerover", prefetch);
89
+ document.removeEventListener("focusin", prefetch);
90
+ };
91
+ }, [router, basePath]);
92
+ return null;
93
+ }
94
+ /**
52
95
  * On a site in several languages, says the page's language on `<html>`, which
53
96
  * the root layout shared by every page can't. `goodfellow-next finish` writes
54
97
  * it into exported pages; this keeps it right as visitors move between pages.
@@ -74,4 +117,4 @@ function ByLanguage({ variants }) {
74
117
  });
75
118
  }
76
119
  //#endregion
77
- export { ByLanguage, CurrentMenuLinks, NextImage, NextLink, PageLanguage };
120
+ export { ByLanguage, CurrentMenuLinks, NextImage, NextLink, PageLanguage, SearchResultLinks };
package/dist/config.d.ts CHANGED
@@ -23,8 +23,10 @@ export interface GoodfellowNextOptions {
23
23
  * - Serves images as they are unless the site sets an image loader, since static files can't be optimized on request.
24
24
  * - Adds the site's redirects (Site Settings → Redirects) ahead of the site's own, for hosts that
25
25
  * send them, such as Vercel. Others get the pages `goodfellow-next finish` writes at old addresses.
26
- * - For a site with an admin address, adds header rules that sandbox every page there but the
27
- * admin panel's own, for hosts that send headers, such as Vercel.
26
+ * - Adds header rules for hosts that send them, such as Vercel: the media library's SVGs
27
+ * sandboxed, the admin panel unframed, and, for a site with an admin address, every page
28
+ * there sandboxed but the admin panel's own, with redirects sending pages opened at the
29
+ * wrong one of the two addresses to the right one.
28
30
  * - With `{ server: true }`, builds a full-stack site for a server instead of as static
29
31
  * files: see `goodfellowApi()` in `@goodfellow-cms/next/server`.
30
32
  * - In `next dev`, runs the admin panel's local backend, so publishing saves to
package/dist/config.js CHANGED
@@ -1,9 +1,10 @@
1
- import { CALENDARS_DIR, FEEDS_DIR, allPages, brokerOrigin, configAdmin, configBroker, loadSiteContent, normalizeBase, redirectFrom, redirectProblems, shownContent, vercelAdminHeaders } from "@goodfellow-cms/core";
2
- import { BUILD_DATES_DIR, BUILD_DATES_ENV, DEV_API_PREFIX, builtSiteSource, contentRepositoryProblem, handleDevApi, isLocalOrigin, localFileStore, localRegistryDir } from "@goodfellow-cms/core/node";
1
+ import { n as IMAGE_SIZES_MARK, t as IMAGE_SIZES_ENV } from "./image-sizes-mark-Ctz9p44g.js";
2
+ import { CALENDARS_DIR, FEEDS_DIR, allPages, brokerOrigin, buildNotes, configAdmin, configBroker, loadSiteContent, normalizeBase, redirectFrom, redirectProblems, shownContent, vercelAdminRedirects, vercelHeaders } from "@goodfellow-cms/core";
3
+ import { BUILD_DATES_DIR, BUILD_DATES_ENV, DEV_API_PREFIX, builtSiteSource, contentRepositoryProblem, handleDevApi, hostFonts, isLocalOrigin, localFileStore, localRegistryDir } from "@goodfellow-cms/core/node";
3
4
  import { existsSync, readFileSync } from "node:fs";
4
- import { rm } from "node:fs/promises";
5
+ import { mkdir, rm, writeFile } from "node:fs/promises";
5
6
  import { createServer } from "node:http";
6
- import { join, resolve } from "node:path";
7
+ import { dirname, join, resolve } from "node:path";
7
8
  //#region src/config.ts
8
9
  const CONFIG_FILES = [
9
10
  "goodfellow.config.tsx",
@@ -17,15 +18,34 @@ function siteAdmin(root) {
17
18
  return file ? configAdmin(readFileSync(file, "utf8")) : void 0;
18
19
  }
19
20
  /**
20
- * The admin address's header rules, for hosts that read them from the build,
21
- * such as Vercel: every page there sandboxed but the admin panel's own, as
22
- * `goodfellow build`'s `_headers` does on Cloudflare Pages. Next.js adds the base path itself.
21
+ * The site's header rules, for hosts that read them from the build, such as
22
+ * Vercel, as `goodfellow build`'s `_headers` does on Cloudflare Pages: the
23
+ * media library's SVGs sandboxed, the admin panel unframed, and with an admin
24
+ * address every page there sandboxed but the admin panel's own. Next.js adds the base path itself.
23
25
  */
24
- function adminHeaders(root) {
26
+ function siteHeaders(root) {
25
27
  const admin = siteAdmin(root);
26
28
  const file = CONFIG_FILES.map((name) => join(root, name)).find((path) => existsSync(path));
27
29
  const broker = brokerOrigin(file ? configBroker(readFileSync(file, "utf8")) : void 0);
28
- return admin ? vercelAdminHeaders(admin, "/", broker) : [];
30
+ return vercelHeaders({
31
+ ...admin && { admin },
32
+ ...broker && { broker }
33
+ });
34
+ }
35
+ /**
36
+ * A full-stack site's Google Fonts, copied into its `public/` folder
37
+ * (`hostFonts()`), or `undefined` where it has none or Google can't be
38
+ * reached, which the build says, as `goodfellow build` does.
39
+ */
40
+ async function hostSiteFonts(root) {
41
+ const { settings } = await loadSiteContent(builtSiteSource(root));
42
+ const fonts = await hostFonts(join(root, "public"), settings.theme);
43
+ if (!fonts) return void 0;
44
+ if ("unavailable" in fonts) {
45
+ for (const line of buildNotes({ fonts })) console.warn(line);
46
+ return;
47
+ }
48
+ return fonts;
29
49
  }
30
50
  /** `PHASE_DEVELOPMENT_SERVER` from `next/constants`, which Node can't import by that name. */
31
51
  const PHASE_DEVELOPMENT_SERVER = "phase-development-server";
@@ -41,13 +61,13 @@ function startDevApi(root) {
41
61
  globals[SERVER_KEY] ??= new Promise((resolvePort, reject) => {
42
62
  const store = localFileStore(root);
43
63
  const registryDir = localRegistryDir(root);
44
- const editor = import("./dev-editor-CzE9HofK.js").then(({ startDevEditor }) => startDevEditor(root)).catch((error) => {
64
+ const editor = import("./dev-editor-BYUVUdPI.js").then(({ startDevEditor }) => startDevEditor(root)).catch((error) => {
45
65
  console.error(`The editor's frames can't be served: ${error.message}`);
46
66
  return { serve: async () => false };
47
67
  });
48
68
  const tasks = {
49
69
  root,
50
- plugins: async () => (await (await import("./plugins-K5i8XztM.js").then((n) => n.o)).loadSiteConfig(root)).plugins ?? [],
70
+ plugins: async () => (await (await import("./plugins-UKkfu0Uj.js").then((n) => n.o)).loadSiteConfig(root)).plugins ?? [],
51
71
  server: "next-dev"
52
72
  };
53
73
  const server = createServer(async (req, res) => {
@@ -116,6 +136,27 @@ async function siteRedirects(root) {
116
136
  });
117
137
  }
118
138
  /**
139
+ * Pages opened at the wrong one of the Site address and the admin address,
140
+ * sent to the right one before anything loads, for hosts that send redirects,
141
+ * such as Vercel (`vercelAdminRedirects()`). The Site address is
142
+ * `content/site.json`'s own, never a copy's, which leaves the admin address
143
+ * out. Their addresses already have the base path, so Next.js adds none.
144
+ */
145
+ function adminRedirects(root, basePath) {
146
+ const admin = siteAdmin(root);
147
+ if (!admin) return [];
148
+ let siteUrl;
149
+ try {
150
+ siteUrl = JSON.parse(readFileSync(join(root, "content/site.json"), "utf8")).url;
151
+ } catch {
152
+ return [];
153
+ }
154
+ return vercelAdminRedirects(admin, typeof siteUrl === "string" ? siteUrl : void 0, `${basePath}/`).map((rule) => ({
155
+ ...rule,
156
+ basePath: false
157
+ }));
158
+ }
159
+ /**
119
160
  * Sets up Next.js for a Goodfellow site, in `next.config.ts`:
120
161
  * `export default withGoodfellow({ ... })`.
121
162
  *
@@ -127,8 +168,10 @@ async function siteRedirects(root) {
127
168
  * - Serves images as they are unless the site sets an image loader, since static files can't be optimized on request.
128
169
  * - Adds the site's redirects (Site Settings → Redirects) ahead of the site's own, for hosts that
129
170
  * send them, such as Vercel. Others get the pages `goodfellow-next finish` writes at old addresses.
130
- * - For a site with an admin address, adds header rules that sandbox every page there but the
131
- * admin panel's own, for hosts that send headers, such as Vercel.
171
+ * - Adds header rules for hosts that send them, such as Vercel: the media library's SVGs
172
+ * sandboxed, the admin panel unframed, and, for a site with an admin address, every page
173
+ * there sandboxed but the admin panel's own, with redirects sending pages opened at the
174
+ * wrong one of the two addresses to the right one.
132
175
  * - With `{ server: true }`, builds a full-stack site for a server instead of as static
133
176
  * files: see `goodfellowApi()` in `@goodfellow-cms/next/server`.
134
177
  * - In `next dev`, runs the admin panel's local backend, so publishing saves to
@@ -160,11 +203,23 @@ function withGoodfellow(nextConfig = {}, options = {}) {
160
203
  force: true
161
204
  });
162
205
  process.env[BUILD_DATES_ENV] = dates;
206
+ const mark = join(root, nextConfig.distDir ?? ".next", IMAGE_SIZES_MARK);
207
+ await rm(mark, { force: true });
208
+ if (!options.server && config.images?.unoptimized) {
209
+ process.env[IMAGE_SIZES_ENV] = "1";
210
+ await mkdir(dirname(mark), { recursive: true });
211
+ await writeFile(mark, "");
212
+ } else delete process.env[IMAGE_SIZES_ENV];
163
213
  }
164
214
  if (phase !== PHASE_DEVELOPMENT_SERVER && options.server) {
165
215
  if (siteAdmin(root)) throw new Error("A full-stack site can't have an admin address: its admin panel signs in with the site's own sign-in.");
216
+ const fonts = phase === PHASE_PRODUCTION_BUILD ? await hostSiteFonts(root) : void 0;
166
217
  return {
167
218
  ...config,
219
+ ...fonts && { env: {
220
+ ...config.env,
221
+ GOODFELLOW_FONTS: JSON.stringify(fonts)
222
+ } },
168
223
  images: nextConfig.images,
169
224
  outputFileTracingIncludes: {
170
225
  ...nextConfig.outputFileTracingIncludes,
@@ -174,14 +229,19 @@ function withGoodfellow(nextConfig = {}, options = {}) {
174
229
  "./public/media/**/*"
175
230
  ]
176
231
  },
177
- redirects: async () => [...await siteRedirects(root), ...await nextConfig.redirects?.() ?? []]
232
+ redirects: async () => [...await siteRedirects(root), ...await nextConfig.redirects?.() ?? []],
233
+ headers: async () => [...siteHeaders(root), ...await nextConfig.headers?.() ?? []]
178
234
  };
179
235
  }
180
236
  if (phase !== PHASE_DEVELOPMENT_SERVER) return {
181
237
  output: "export",
182
238
  ...config,
183
- redirects: async () => [...await siteRedirects(root), ...await nextConfig.redirects?.() ?? []],
184
- ...siteAdmin(root) && { headers: async () => [...adminHeaders(root), ...await nextConfig.headers?.() ?? []] }
239
+ redirects: async () => [
240
+ ...adminRedirects(root, basePath),
241
+ ...await siteRedirects(root),
242
+ ...await nextConfig.redirects?.() ?? []
243
+ ],
244
+ ...(siteAdmin(root) || process.env.VERCEL) && { headers: async () => [...siteHeaders(root), ...await nextConfig.headers?.() ?? []] }
185
245
  };
186
246
  const port = await startDevApi(root);
187
247
  const rewrite = {
@@ -1,4 +1,4 @@
1
- import { i as loadSiteConfig, t as buildPluginFrame } from "./plugins-K5i8XztM.js";
1
+ import { i as loadSiteConfig, t as buildPluginFrame } from "./plugins-UKkfu0Uj.js";
2
2
  import { DEV_EDITOR_PREFIX, EDITOR_PRELUDE, editorPage, pluginId } from "@goodfellow-cms/core";
3
3
  import { existsSync } from "node:fs";
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
package/dist/export.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { PluginTaskRequest, SelfHostedResult, SiteProblem, TaskResultFile } from "@goodfellow-cms/core";
2
- import { SiteFile } from "@goodfellow-cms/core/node";
1
+ import { GoodfellowConfig, PluginTaskRequest, SelfHostedResult, SiteProblem, TaskResultFile } from "@goodfellow-cms/core";
2
+ import { ImageSizesResult, SiteFile } from "@goodfellow-cms/core/node";
3
3
  import { PageRender } from "@goodfellow-cms/core/shadcn";
4
4
  //#region src/site-files.d.ts
5
5
  /**
@@ -84,6 +84,19 @@ export interface RedirectsWritten {
84
84
  * has a page at keeps it.
85
85
  */
86
86
  export declare function writeRedirectFiles(outDir: string, root?: string): Promise<RedirectsWritten>;
87
+ /**
88
+ * Writes the smaller copies of the media library's photos that the export's
89
+ * pages list in their `srcset` (`writeImageSizes()`), with sharp, where
90
+ * `next build` said they list them (`IMAGE_SIZES_MARK`): a static export
91
+ * whose images Next.js doesn't optimize. `undefined` where they don't.
92
+ */
93
+ export declare function writeImageFiles(outDir: string, root?: string): Promise<ImageSizesResult | undefined>;
94
+ /**
95
+ * Adds Cloudflare Pages' header rules for every site to the export's
96
+ * `_headers`, as `goodfellow build` does (`writeSiteHeaders()`), from the
97
+ * admin address and broker the exported `goodfellow-site.json` names.
98
+ */
99
+ export declare function writeExportHeaders(outDir: string, root?: string): Promise<void>;
87
100
  /**
88
101
  * For a site with an admin address (its config's `admin`, which the exported
89
102
  * `goodfellow-site.json` carries), puts the guard first in every page of the
@@ -145,10 +158,12 @@ export declare function checkPages(pages: readonly BuiltPage[], root?: string):
145
158
  /**
146
159
  * Runs one of a plugin's tasks for the admin panel, as `goodfellow task`
147
160
  * does for other sites, with the site's config loaded in Node, and writes its
148
- * answer sealed to the request's key, even when the task fails. Secrets are
149
- * taken out of the environment before the config loads.
161
+ * answer sealed to the request's key, even when the task fails. The config
162
+ * loads in a process of its own, with none of the job's secrets.
150
163
  */
151
164
  export declare function writeTaskResult(root: string, request: PluginTaskRequest, out?: string): Promise<TaskResultFile>;
165
+ /** The site's config, loaded in Node, for `writeTaskResult()`'s process that loads it apart from the job's secrets. */
166
+ export declare function loadTaskConfig(root: string): Promise<GoodfellowConfig>;
152
167
  /**
153
168
  * Renders a page with a block from its own file as well as the site's config's,
154
169
  * for `goodfellow-next blocks add`'s check that a block it made renders: both
package/dist/export.js CHANGED
@@ -1,7 +1,8 @@
1
- import { a as loadSiteModule, i as loadSiteConfig, n as buildPluginFrames, r as configHasPlugins } from "./plugins-K5i8XztM.js";
1
+ import { a as loadSiteModule, i as loadSiteConfig, n as buildPluginFrames, r as configHasPlugins } from "./plugins-UKkfu0Uj.js";
2
+ import "./image-sizes-mark-Ctz9p44g.js";
2
3
  import { r as NEXT_SITE_FILES } from "./site-files-BeLnLxCl.js";
3
4
  import { CALENDARS_DIR, CLOUDFLARE_REDIRECTS_PATH, EDITOR_PRELUDE, FEEDS_DIR, SITE_DESCRIPTION_PATH, TASK_RESULT_FILE, absoluteUrl, allPages, calendarFiles, cloudflareRedirects, editorPolicy, feedFiles, hasLanguages, loadSiteContent, parseSiteDescription, pathLanguage, pluginId, redirectFiles, rootRobotsUrl, sealTaskAnswer, shownContent, siteLanguages, sitePages, taskRequestProblem, todayIn } from "@goodfellow-cms/core";
4
- import { BUILD_DATES_DIR, builtSiteSource, guardBuiltSite, reportedDate, runPluginBuildSteps, runPluginTask, selfHostFonts, taskSecretsFrom, writeBuildRecord } from "@goodfellow-cms/core/node";
5
+ import { BUILD_DATES_DIR, builtSiteSource, guardBuiltSite, reportedDate, runPluginBuildSteps, runPluginTask, selfHostFonts, sitePluginsApart, taskSecretsFrom, writeBuildRecord, writeImageSizes, writeSiteHeaders } from "@goodfellow-cms/core/node";
5
6
  import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
6
7
  import { dirname, join, relative, resolve } from "node:path";
7
8
  import { createHash } from "node:crypto";
@@ -177,6 +178,41 @@ async function writeRedirectFiles(outDir, root = process.cwd()) {
177
178
  };
178
179
  }
179
180
  /**
181
+ * Writes the smaller copies of the media library's photos that the export's
182
+ * pages list in their `srcset` (`writeImageSizes()`), with sharp, where
183
+ * `next build` said they list them (`IMAGE_SIZES_MARK`): a static export
184
+ * whose images Next.js doesn't optimize. `undefined` where they don't.
185
+ */
186
+ async function writeImageFiles(outDir, root = process.cwd()) {
187
+ const dir = resolve(root);
188
+ if (!await exists(join(dir, ".next", "goodfellow-image-sizes"))) return void 0;
189
+ return writeImageSizes(dir, outDir, { resize: await sharpResizer() });
190
+ }
191
+ /** Makes the copies with sharp, turned as each photo's details say and without them, unless it can't load here. */
192
+ async function sharpResizer() {
193
+ try {
194
+ const { default: sharp } = await import("sharp");
195
+ return async (image, width) => new Uint8Array(await sharp(image).rotate().resize({ width }).toBuffer());
196
+ } catch {
197
+ return;
198
+ }
199
+ }
200
+ /**
201
+ * Adds Cloudflare Pages' header rules for every site to the export's
202
+ * `_headers`, as `goodfellow build` does (`writeSiteHeaders()`), from the
203
+ * admin address and broker the exported `goodfellow-site.json` names.
204
+ */
205
+ async function writeExportHeaders(outDir, root = process.cwd()) {
206
+ const text = await readFile(join(outDir, SITE_DESCRIPTION_PATH), "utf8").catch(() => void 0);
207
+ const parsed = text === void 0 ? void 0 : parseSiteDescription(text);
208
+ const description = parsed?.ok ? parsed.description : void 0;
209
+ await writeSiteHeaders(outDir, {
210
+ base: `${await exportBasePath(resolve(root))}/`,
211
+ ...description?.admin && { admin: description.admin },
212
+ ...description?.broker && { broker: description.broker }
213
+ });
214
+ }
215
+ /**
180
216
  * For a site with an admin address (its config's `admin`, which the exported
181
217
  * `goodfellow-site.json` carries), puts the guard first in every page of the
182
218
  * export and adds Cloudflare Pages' header rules, as `goodfellow build` does.
@@ -355,8 +391,8 @@ async function checkPages(pages, root = process.cwd()) {
355
391
  /**
356
392
  * Runs one of a plugin's tasks for the admin panel, as `goodfellow task`
357
393
  * does for other sites, with the site's config loaded in Node, and writes its
358
- * answer sealed to the request's key, even when the task fails. Secrets are
359
- * taken out of the environment before the config loads.
394
+ * answer sealed to the request's key, even when the task fails. The config
395
+ * loads in a process of its own, with none of the job's secrets.
360
396
  */
361
397
  async function writeTaskResult(root, request, out = TASK_RESULT_FILE) {
362
398
  const secrets = taskSecretsFrom(process.env);
@@ -364,7 +400,10 @@ async function writeTaskResult(root, request, out = TASK_RESULT_FILE) {
364
400
  if (problem) throw new Error(problem);
365
401
  let answer;
366
402
  try {
367
- const plugin = ((await loadSiteConfig(resolve(root))).plugins ?? []).find((candidate) => pluginId(candidate.name) === request.plugin);
403
+ const plugin = (await sitePluginsApart(resolve(root), {
404
+ module: import.meta.url,
405
+ export: "loadTaskConfig"
406
+ })).find((candidate) => pluginId(candidate.name) === request.plugin);
368
407
  answer = plugin ? await runPluginTask({
369
408
  root: resolve(root),
370
409
  plugin,
@@ -385,6 +424,10 @@ async function writeTaskResult(root, request, out = TASK_RESULT_FILE) {
385
424
  await writeFile(resolve(root, out), `${JSON.stringify(sealed)}\n`);
386
425
  return sealed;
387
426
  }
427
+ /** The site's config, loaded in Node, for `writeTaskResult()`'s process that loads it apart from the job's secrets. */
428
+ function loadTaskConfig(root) {
429
+ return loadSiteConfig(root);
430
+ }
388
431
  /**
389
432
  * Renders a page with a block from its own file as well as the site's config's,
390
433
  * for `goodfellow-next blocks add`'s check that a block it made renders: both
@@ -410,4 +453,4 @@ function blockPageRender(root) {
410
453
  };
411
454
  }
412
455
  //#endregion
413
- export { EDITOR_PAGES, NEXT_SITE_FILES, blockPageRender, buildFileFolders, builtPages, checkPages, exportedPages, fixSegmentFiles, guardExport, writeBuildRecordFile, writeCalendarFiles, writeContentApi, writeEditorPolicy, writeFeedFiles, writeFontFiles, writePageLanguages, writePluginFiles, writeRedirectFiles, writeTaskResult };
456
+ export { EDITOR_PAGES, NEXT_SITE_FILES, blockPageRender, buildFileFolders, builtPages, checkPages, exportedPages, fixSegmentFiles, guardExport, loadTaskConfig, writeBuildRecordFile, writeCalendarFiles, writeContentApi, writeEditorPolicy, writeExportHeaders, writeFeedFiles, writeFontFiles, writeImageFiles, writePageLanguages, writePluginFiles, writeRedirectFiles, writeTaskResult };
@@ -0,0 +1,11 @@
1
+ //#region src/image-sizes-mark.ts
2
+ /**
3
+ * Set while `next build` renders a static export whose images Next.js doesn't
4
+ * optimize, so pages list the smaller copies of the media library's photos
5
+ * (`imageWidths`) that `goodfellow-next finish` writes.
6
+ */
7
+ const IMAGE_SIZES_ENV = "GOODFELLOW_IMAGE_SIZES";
8
+ /** The file in `.next/` that tells `goodfellow-next finish` the pages list them. */
9
+ const IMAGE_SIZES_MARK = "goodfellow-image-sizes";
10
+ //#endregion
11
+ export { IMAGE_SIZES_MARK as n, IMAGE_SIZES_ENV as t };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { ByLanguage, CurrentMenuLinks, NextImage, NextLink, PageLanguage } from "./components.js";
2
- import { absoluteUrl, adminSiteForCopy, colorSchemeMeta, colorSchemeScript, contentForPage, demoContent, describedForCopy, feedLinks, findRedirect, fontPreloads, googleFontsUrl, hasLanguages, languageMap, loadSiteContent, normalizePagePath, pageHead, shownContent, siteCopyFrom, siteLanguages, sitePages, sitemapPages, storedMediaSizes, themeToCss, todayIn, withBase } from "@goodfellow-cms/core";
1
+ import { ByLanguage, CurrentMenuLinks, NextImage, NextLink, PageLanguage, SearchResultLinks } from "./components.js";
2
+ import "./image-sizes-mark-Ctz9p44g.js";
3
+ import { BLOCK_SCRIPTS, DARK_MODE_ATTRIBUTE, IMAGE_WIDTHS, absoluteUrl, adminSiteForCopy, colorSchemeMeta, colorSchemeScript, consentScript, contentForPage, demoContent, describedForCopy, feedLinks, findRedirect, fontPreloads, googleFontsUrl, hasLanguages, heldCode, languageMap, loadSiteContent, normalizePagePath, pageHead, shownContent, siteCopyFrom, siteLanguages, sitePages, sitemapPages, storedMediaSizes, themeToCss, todayIn, withBase, withSelfHostedFonts } from "@goodfellow-cms/core";
3
4
  import { BUILD_DATES_ENV, builtSiteSource, fileSystemSource, fontStandIns, readMediaSizes, readStylesheet, reportDatesTo } from "@goodfellow-cms/core/node";
4
5
  import { BodyCode, HeadCode, PageBody, PageContent, PageFooter, PageHeader, adminSite, applyPageEntry, createPuckConfigs, describeSite, prepareLayout, preparePage } from "@goodfellow-cms/react";
5
6
  import { notFound, redirect } from "next/navigation";
@@ -19,6 +20,22 @@ function configuredBasePath() {
19
20
  return process.env.GOODFELLOW_BASE_PATH ?? "";
20
21
  }
21
22
  /**
23
+ * Where a full-stack site's build copied its Google Fonts (`hostFonts()`), for
24
+ * the stylesheet at `source`. `withGoodfellow()` puts them in the env, which
25
+ * Next.js writes into this code by its name, so pages it renders later have
26
+ * them too. Static exports have none: `goodfellow-next finish` copies theirs.
27
+ */
28
+ function hostedFonts(source) {
29
+ const value = process.env.GOODFELLOW_FONTS;
30
+ if (!value) return void 0;
31
+ try {
32
+ const fonts = JSON.parse(value);
33
+ return fonts.source === source ? fonts : void 0;
34
+ } catch {
35
+ return;
36
+ }
37
+ }
38
+ /**
22
39
  * Reads the site's content from disk, as visitors see it now: without pages
23
40
  * and items scheduled for later, or hidden again (`shownContent()`). Cached
24
41
  * for the length of one request, so a page and its metadata read it once, and
@@ -39,6 +56,26 @@ function exportedPages(content, blocks) {
39
56
  function findPage(content, blocks, path) {
40
57
  return exportedPages(content, blocks).find((page) => page.path === path);
41
58
  }
59
+ /** Whether the site asks visitors before loading other sites' content. */
60
+ function asks(content) {
61
+ return content.settings.consent !== void 0;
62
+ }
63
+ /** The consent script, after the footer's notice, on a site that asks. */
64
+ function ConsentAsk({ content }) {
65
+ if (!asks(content)) return null;
66
+ return /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: consentScript() } });
67
+ }
68
+ /**
69
+ * What the built-in blocks need in the browser (copying code, the Dark mode
70
+ * switch, search), each a small script that works on what navigations add.
71
+ * `goodfellow build` adds each only to pages that use it; a layout can't know
72
+ * the page, so it has them all, but the Dark mode switch's only where the
73
+ * site has dark mode, since it shows nothing otherwise.
74
+ */
75
+ function BlockScripts({ content }) {
76
+ const scripts = BLOCK_SCRIPTS.filter(({ attribute }) => attribute !== DARK_MODE_ATTRIBUTE || content.settings.theme.darkMode);
77
+ return /* @__PURE__ */ jsx(Fragment, { children: scripts.map(({ attribute, script }) => /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: script() } }, attribute)) });
78
+ }
42
79
  function toAbsolute(siteUrl, url) {
43
80
  if (!url || !siteUrl || !url.startsWith("/") || url.startsWith("//")) return url;
44
81
  return absoluteUrl(siteUrl, url);
@@ -90,17 +127,24 @@ function goodfellowPages(config, options = {}) {
90
127
  media: {
91
128
  ...await readMediaSizes(root),
92
129
  ...storedMediaSizes((await site()).mediaStorage)
93
- }
130
+ },
131
+ ...process.env["GOODFELLOW_IMAGE_SIZES"] && { imageWidths: IMAGE_WIDTHS }
94
132
  });
95
133
  /** The site's theme and fonts, which React moves into the document's head. themeToCss escapes its output. */
96
134
  async function Theme({ content }) {
97
- const fontsUrl = googleFontsUrl(content.settings.theme);
135
+ const googleUrl = googleFontsUrl(content.settings.theme);
136
+ const hosted = googleUrl ? hostedFonts(googleUrl) : void 0;
137
+ const fontsUrl = googleUrl && hosted ? withSelfHostedFonts(googleUrl, hosted, basePath()) : googleUrl;
98
138
  const [fontFiles, standIns] = await Promise.all([fontPreloads(content.settings.theme, readStylesheet), fontStandIns(content.settings.theme)]);
99
- for (const href of fontFiles) preload(href, {
100
- as: "font",
101
- type: "font/woff2",
102
- crossOrigin: ""
103
- });
139
+ for (const file of fontFiles) {
140
+ const href = hosted ? withSelfHostedFonts(file, hosted, basePath()) : file;
141
+ if (hosted && href === file) continue;
142
+ preload(href, {
143
+ as: "font",
144
+ type: "font/woff2",
145
+ crossOrigin: ""
146
+ });
147
+ }
104
148
  const colorScheme = colorSchemeMeta(content.settings.theme);
105
149
  const schemeScript = colorSchemeScript(content.settings.theme);
106
150
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -197,12 +241,22 @@ function goodfellowPages(config, options = {}) {
197
241
  }
198
242
  return /* @__PURE__ */ jsxs(Fragment, { children: [
199
243
  /* @__PURE__ */ jsx(Theme, { content }),
200
- /* @__PURE__ */ jsx(HeadCode, { code: content.code?.head }),
244
+ /* @__PURE__ */ jsx(HeadCode, {
245
+ code: content.code?.head,
246
+ held: heldCode(content.settings.consent, "head"),
247
+ siteUrl: content.settings.url
248
+ }),
201
249
  header,
202
250
  children,
203
251
  footer,
204
252
  /* @__PURE__ */ jsx(CurrentMenuLinks, { basePath: basePath() }),
205
- /* @__PURE__ */ jsx(BodyCode, { code: content.code?.body })
253
+ /* @__PURE__ */ jsx(SearchResultLinks, { basePath: basePath() }),
254
+ /* @__PURE__ */ jsx(BodyCode, {
255
+ code: content.code?.body,
256
+ held: heldCode(content.settings.consent, "body")
257
+ }),
258
+ /* @__PURE__ */ jsx(ConsentAsk, { content }),
259
+ /* @__PURE__ */ jsx(BlockScripts, { content })
206
260
  ] });
207
261
  },
208
262
  /** The page at the route's address, inside `Layout`'s header and footer. */
@@ -237,7 +291,11 @@ function goodfellowPages(config, options = {}) {
237
291
  return /* @__PURE__ */ jsxs(Fragment, { children: [
238
292
  /* @__PURE__ */ jsx("title", { children: pageHead(content, page).title }),
239
293
  /* @__PURE__ */ jsx(Theme, { content }),
240
- /* @__PURE__ */ jsx(HeadCode, { code: content.code?.head }),
294
+ /* @__PURE__ */ jsx(HeadCode, {
295
+ code: content.code?.head,
296
+ held: heldCode(content.settings.consent, "head"),
297
+ siteUrl: content.settings.url
298
+ }),
241
299
  /* @__PURE__ */ jsx(PageBody, {
242
300
  site: prepared.site,
243
301
  pageConfig: prepared.pageConfig,
@@ -246,7 +304,13 @@ function goodfellowPages(config, options = {}) {
246
304
  header: prepared.header,
247
305
  footer: prepared.footer
248
306
  }),
249
- /* @__PURE__ */ jsx(BodyCode, { code: content.code?.body })
307
+ /* @__PURE__ */ jsx(BodyCode, {
308
+ code: content.code?.body,
309
+ held: heldCode(content.settings.consent, "body")
310
+ }),
311
+ /* @__PURE__ */ jsx(ConsentAsk, { content }),
312
+ /* @__PURE__ */ jsx(BlockScripts, { content }),
313
+ /* @__PURE__ */ jsx(SearchResultLinks, { basePath: basePath() })
250
314
  ] });
251
315
  },
252
316
  /** For `app/sitemap.ts`: every page but "Page not found" and pages hidden from search engines, once the site's address is set. */
@@ -1,5 +1,5 @@
1
1
  import { EDITOR_PRELUDE, INSTALLED_PLUGINS_INDEX, PLUGINS_PATH, framedPlugins, pluginFrameEntry, pluginFramePage, pluginId } from "@goodfellow-cms/core";
2
- import { LICENSES_FILE, licensesPlugin } from "@goodfellow-cms/core/node";
2
+ import { LICENSES_FILE, licensesPlugin, mediaStorageOrigins } from "@goodfellow-cms/core/node";
3
3
  import { existsSync } from "node:fs";
4
4
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
5
5
  import { isAbsolute, join, resolve } from "node:path";
@@ -125,7 +125,8 @@ async function buildPluginFrame(plugin, { root, dir, watch }) {
125
125
  scripts: ["plugin.js"],
126
126
  stylesheets: ["plugin.css"],
127
127
  licenses: LICENSES_FILE,
128
- editor: Boolean(plugin.editor)
128
+ editor: Boolean(plugin.editor),
129
+ ...plugin.editor && { media: await mediaStorageOrigins(root) }
129
130
  }));
130
131
  };
131
132
  const options = {
package/dist/server.d.ts CHANGED
@@ -38,6 +38,13 @@ export interface GoodfellowApiOptions {
38
38
  * `plugins/<id>/…`, and runs with everything the server has.
39
39
  */
40
40
  plugins?: AppApiOptions["plugins"];
41
+ /**
42
+ * Whether owners' changes to the site's code, such as adding blocks on the
43
+ * Blocks screen, go to a developer for review (`"review"`, the default),
44
+ * since this server runs that code with its token and keys, or are published
45
+ * (`"publish"`).
46
+ */
47
+ codeChanges?: AppApiOptions["codeChanges"];
41
48
  }
42
49
  /** The route parameters of a catch-all route named `[...path]`. */
43
50
  interface RouteContext {
package/dist/server.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ownersOnlyFields } from "@goodfellow-cms/core";
1
2
  import { appApi } from "@goodfellow-cms/core/node";
2
3
  import { aiServer } from "@goodfellow-cms/ai";
3
4
  //#region src/server.ts
@@ -32,7 +33,9 @@ function goodfellowApi(options) {
32
33
  ...options.signOut && { signOut: options.signOut },
33
34
  ...ai && ai.services.length > 0 && { ai },
34
35
  ...options.onError && { onError: options.onError },
35
- ...options.plugins && { plugins: options.plugins }
36
+ ...options.plugins && { plugins: options.plugins },
37
+ ...options.codeChanges && { codeChanges: options.codeChanges },
38
+ ownersOnly: ownersOnlyFields(options.config.blocks ?? {})
36
39
  });
37
40
  return api;
38
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodfellow-cms/next",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Goodfellow pages and the admin panel in a Next.js site.",
5
5
  "keywords": [
6
6
  "goodfellow",
@@ -63,23 +63,24 @@
63
63
  "dist"
64
64
  ],
65
65
  "dependencies": {
66
- "@goodfellow-cms/ai": "0.7.0",
67
- "@goodfellow-cms/core": "0.7.0",
68
- "@goodfellow-cms/react": "0.7.0",
69
- "rolldown": "1.2.12"
66
+ "@goodfellow-cms/ai": "0.8.0",
67
+ "@goodfellow-cms/core": "0.8.0",
68
+ "@goodfellow-cms/react": "0.8.0",
69
+ "rolldown": "1.2.12",
70
+ "sharp": "0.35.5"
70
71
  },
71
72
  "peerDependencies": {
72
- "@goodfellow-cms/admin": "0.7.0",
73
+ "@goodfellow-cms/admin": "0.8.0",
73
74
  "@puckeditor/core": "0.23.0",
74
75
  "next": "^16.0.0",
75
76
  "react": "^19.0.0",
76
77
  "react-dom": "^19.0.0"
77
78
  },
78
79
  "devDependencies": {
79
- "@goodfellow-cms/admin": "0.7.0",
80
- "@goodfellow-cms/bitbucket": "0.7.0",
81
- "@goodfellow-cms/github": "0.7.0",
82
- "@goodfellow-cms/gitlab": "0.7.0",
80
+ "@goodfellow-cms/admin": "0.8.0",
81
+ "@goodfellow-cms/bitbucket": "0.8.0",
82
+ "@goodfellow-cms/github": "0.8.0",
83
+ "@goodfellow-cms/gitlab": "0.8.0",
83
84
  "@puckeditor/core": "0.23.0",
84
85
  "@tailwindcss/browser": "4.3.3",
85
86
  "@types/react": "^19.2.0",