@goodfellow-cms/next 0.6.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.
- package/bin/goodfellow-next.js +47 -2
- package/dist/admin.js +1 -1
- package/dist/components.d.ts +11 -1
- package/dist/components.js +46 -3
- package/dist/config.d.ts +4 -2
- package/dist/config.js +77 -17
- package/dist/{dev-editor-EIZQe3qO.js → dev-editor-BYUVUdPI.js} +1 -1
- package/dist/export.d.ts +34 -4
- package/dist/export.js +110 -16
- package/dist/image-sizes-mark-Ctz9p44g.js +11 -0
- package/dist/index.js +81 -17
- package/dist/{plugins-eLJalBij.js → plugins-UKkfu0Uj.js} +19 -12
- package/dist/server.d.ts +7 -0
- package/dist/server.js +4 -1
- package/package.json +11 -10
package/bin/goodfellow-next.js
CHANGED
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
// content is kept in a repository of its own.
|
|
11
11
|
// `goodfellow-next import wordpress <file or address> [--dry-run]` imports a WordPress site, as `goodfellow import` does.
|
|
12
12
|
// `goodfellow-next export wordpress [file]` writes the site as a WordPress export file, as `goodfellow export` does.
|
|
13
|
+
// `goodfellow-next blocks add <item> [--registry <folder>] [--native <app>]` makes a block of a shadcn item, as
|
|
14
|
+
// `goodfellow blocks add` does, and with --native its version for an Expo app.
|
|
13
15
|
// `goodfellow-next admin add <address>|check|remove` sets the admin address, and `goodfellow-next broker use <address>|remove`
|
|
14
16
|
// the token broker, in goodfellow.config.tsx, as `goodfellow admin` and `goodfellow broker` do.
|
|
15
17
|
import { existsSync, readFileSync } from "node:fs";
|
|
16
18
|
import { rm } from "node:fs/promises";
|
|
17
|
-
import { relative } from "node:path";
|
|
19
|
+
import { relative, resolve } from "node:path";
|
|
18
20
|
import { parseArgs } from "node:util";
|
|
19
21
|
import { buildNotes, CLOUDFLARE_REDIRECTS_LIMIT, configAdmin, formatUpdateResult } from "@goodfellow-cms/core";
|
|
20
22
|
import { reportSiteProblems } from "@goodfellow-cms/core/accessibility";
|
|
@@ -39,8 +41,10 @@ import {
|
|
|
39
41
|
update,
|
|
40
42
|
} from "@goodfellow-cms/core/node";
|
|
41
43
|
import { writePagesSearchIndex, writeSearchIndex } from "@goodfellow-cms/core/search-index";
|
|
44
|
+
import { addShadcnBlock, blockWorks, formatShadcnBlock, ShadcnBlockError } from "@goodfellow-cms/core/shadcn";
|
|
42
45
|
import { WordPressReadError } from "@goodfellow-cms/core/wordpress";
|
|
43
46
|
import {
|
|
47
|
+
blockPageRender,
|
|
44
48
|
buildFileFolders,
|
|
45
49
|
builtPages,
|
|
46
50
|
checkPages,
|
|
@@ -50,9 +54,12 @@ import {
|
|
|
50
54
|
NEXT_SITE_FILES,
|
|
51
55
|
writeBuildRecordFile,
|
|
52
56
|
writeCalendarFiles,
|
|
57
|
+
writeContentApi,
|
|
53
58
|
writeEditorPolicy,
|
|
59
|
+
writeExportHeaders,
|
|
54
60
|
writeFeedFiles,
|
|
55
61
|
writeFontFiles,
|
|
62
|
+
writeImageFiles,
|
|
56
63
|
writePageLanguages,
|
|
57
64
|
writePluginFiles,
|
|
58
65
|
writeRedirectFiles,
|
|
@@ -80,6 +87,8 @@ if (command === "finish" && dir === "--server") {
|
|
|
80
87
|
if (calendars > 0) console.log(`Wrote ${calendars} calendar files, in public/calendars`);
|
|
81
88
|
const feeds = await writeFeedFiles("public");
|
|
82
89
|
if (feeds > 0) console.log(`Wrote ${feeds} feeds, in public/feeds`);
|
|
90
|
+
const api = await writeContentApi("public");
|
|
91
|
+
if (api !== undefined) console.log(`Wrote the content API, with ${api} pages, in public/api`);
|
|
83
92
|
// Before the pages are read, so the search index and the checks see each page's language.
|
|
84
93
|
const languages = await writePageLanguages(undefined);
|
|
85
94
|
if (languages > 0) console.log(`Marked ${languages} pages with their language`);
|
|
@@ -227,6 +236,31 @@ if (command === "export") {
|
|
|
227
236
|
process.exit(0);
|
|
228
237
|
}
|
|
229
238
|
}
|
|
239
|
+
if (command === "blocks") {
|
|
240
|
+
const { positionals, values } = parseArgs({
|
|
241
|
+
args: process.argv.slice(3),
|
|
242
|
+
allowPositionals: true,
|
|
243
|
+
options: { registry: { type: "string" }, native: { type: "string" } },
|
|
244
|
+
});
|
|
245
|
+
const [action, item] = positionals;
|
|
246
|
+
if (action === "add" && item) {
|
|
247
|
+
try {
|
|
248
|
+
const result = await addShadcnBlock({
|
|
249
|
+
root: process.cwd(),
|
|
250
|
+
item,
|
|
251
|
+
...(values.registry && { registry: resolve(values.registry) }),
|
|
252
|
+
...(values.native && { native: resolve(values.native) }),
|
|
253
|
+
render: blockPageRender("."),
|
|
254
|
+
});
|
|
255
|
+
console.log(formatShadcnBlock(result));
|
|
256
|
+
process.exit(blockWorks(result) ? 0 : 1);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (!(error instanceof ShadcnBlockError)) throw error;
|
|
259
|
+
console.error(error.message);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
230
264
|
if (command !== "finish" && command !== "index") {
|
|
231
265
|
console.error(
|
|
232
266
|
[
|
|
@@ -239,6 +273,7 @@ if (command !== "finish" && command !== "index") {
|
|
|
239
273
|
" goodfellow-next content link|copy [folder]",
|
|
240
274
|
" goodfellow-next import wordpress <file or address> [--dry-run]",
|
|
241
275
|
" goodfellow-next export wordpress [file]",
|
|
276
|
+
" goodfellow-next blocks add <item> [--registry <folder>] [--native <app>]",
|
|
242
277
|
" goodfellow-next admin add <address> | check | remove",
|
|
243
278
|
" goodfellow-next broker use <address> | remove",
|
|
244
279
|
"",
|
|
@@ -269,6 +304,11 @@ if (command !== "finish" && command !== "index") {
|
|
|
269
304
|
" says what it would bring across and changes nothing.",
|
|
270
305
|
"export Writes this site as a WordPress export file (default: named after the site),",
|
|
271
306
|
" which WordPress imports with Tools → Import → WordPress.",
|
|
307
|
+
"blocks Installs a shadcn registry item (button, @acme/hero, or an item's address) with",
|
|
308
|
+
" shadcn's own CLI, makes a block of its component, with fields from its props, and",
|
|
309
|
+
" says what it couldn't do. With --registry, writes the block into the block registry",
|
|
310
|
+
" in folder instead. Needs TypeScript 7 as typescript-7, since Next.js builds with 5:",
|
|
311
|
+
" npm install --save-dev typescript-7@npm:typescript@7.0.2",
|
|
272
312
|
"admin The admin panel's address of its own, such as admin.example.org, in",
|
|
273
313
|
" goodfellow.config.tsx: add names it; check checks it once the site has rebuilt with",
|
|
274
314
|
" it, then starts the admin panel only there; remove stops using it.",
|
|
@@ -288,6 +328,8 @@ if (command === "finish") {
|
|
|
288
328
|
if (calendars > 0) console.log(`Wrote ${calendars} calendar files`);
|
|
289
329
|
const feeds = await writeFeedFiles(dir);
|
|
290
330
|
if (feeds > 0) console.log(`Wrote ${feeds} feeds`);
|
|
331
|
+
const api = await writeContentApi(dir);
|
|
332
|
+
if (api !== undefined) console.log(`Wrote the content API, with ${api} pages, in api/`);
|
|
291
333
|
const languages = await writePageLanguages(dir);
|
|
292
334
|
if (languages > 0) console.log(`Marked ${languages} pages with their language`);
|
|
293
335
|
// Before the redirects' pages, which aren't the site's.
|
|
@@ -299,7 +341,10 @@ if (command === "finish") {
|
|
|
299
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.`,
|
|
300
342
|
);
|
|
301
343
|
}
|
|
302
|
-
|
|
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);
|
|
303
348
|
// Plugins' admin frames and build steps, once Goodfellow has written the rest, before the index and the guards.
|
|
304
349
|
const plugins = await writePluginFiles(dir);
|
|
305
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
|
/**
|
package/dist/components.d.ts
CHANGED
|
@@ -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
|
package/dist/components.js
CHANGED
|
@@ -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
|
-
* -
|
|
27
|
-
* admin panel
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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) => {
|
|
@@ -101,7 +121,7 @@ function redirectSource(path) {
|
|
|
101
121
|
* Content with a problem gives none, and the build reports the problem.
|
|
102
122
|
*/
|
|
103
123
|
async function siteRedirects(root) {
|
|
104
|
-
const content = await loadSiteContent(
|
|
124
|
+
const content = await loadSiteContent(builtSiteSource(root)).then(shownContent, () => void 0);
|
|
105
125
|
if (!content?.redirects) return [];
|
|
106
126
|
const taken = new Set(allPages(content).map((page) => page.path));
|
|
107
127
|
const problems = redirectProblems(content.redirects);
|
|
@@ -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
|
-
* -
|
|
131
|
-
* admin panel
|
|
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 () => [
|
|
184
|
-
|
|
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-
|
|
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,6 @@
|
|
|
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
|
+
import { PageRender } from "@goodfellow-cms/core/shadcn";
|
|
3
4
|
//#region src/site-files.d.ts
|
|
4
5
|
/**
|
|
5
6
|
* The Next.js starter's files that `goodfellow-next update` keeps up to date
|
|
@@ -20,6 +21,14 @@ export declare function writeCalendarFiles(outDir: string, root?: string): Promi
|
|
|
20
21
|
* build` does. Returns how many it wrote: none without the site's address.
|
|
21
22
|
*/
|
|
22
23
|
export declare function writeFeedFiles(outDir: string, root?: string): Promise<number>;
|
|
24
|
+
/**
|
|
25
|
+
* Writes the content API (`/api/`) into the export, as `goodfellow build`
|
|
26
|
+
* does: the site's content as JSON, each page as it's shown, prepared in Node
|
|
27
|
+
* with the site's config as `goodfellow build` prepares it, so both write the
|
|
28
|
+
* same files. Returns how many pages it has, or `undefined` where the
|
|
29
|
+
* config's `api` is `false`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function writeContentApi(outDir: string, root?: string): Promise<number | undefined>;
|
|
23
32
|
/**
|
|
24
33
|
* The folders the calendar files and feeds go in: `calendars/` and `feeds/`,
|
|
25
34
|
* and on a site in several languages, the same under each other language's
|
|
@@ -75,6 +84,19 @@ export interface RedirectsWritten {
|
|
|
75
84
|
* has a page at keeps it.
|
|
76
85
|
*/
|
|
77
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>;
|
|
78
100
|
/**
|
|
79
101
|
* For a site with an admin address (its config's `admin`, which the exported
|
|
80
102
|
* `goodfellow-site.json` carries), puts the guard first in every page of the
|
|
@@ -136,8 +158,16 @@ export declare function checkPages(pages: readonly BuiltPage[], root?: string):
|
|
|
136
158
|
/**
|
|
137
159
|
* Runs one of a plugin's tasks for the admin panel, as `goodfellow task`
|
|
138
160
|
* does for other sites, with the site's config loaded in Node, and writes its
|
|
139
|
-
* answer sealed to the request's key, even when the task fails.
|
|
140
|
-
*
|
|
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.
|
|
141
163
|
*/
|
|
142
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>;
|
|
167
|
+
/**
|
|
168
|
+
* Renders a page with a block from its own file as well as the site's config's,
|
|
169
|
+
* for `goodfellow-next blocks add`'s check that a block it made renders: both
|
|
170
|
+
* loaded in Node, as `goodfellow-next finish` loads the config.
|
|
171
|
+
*/
|
|
172
|
+
export declare function blockPageRender(root: string): PageRender;
|
|
143
173
|
//#endregion
|
package/dist/export.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { i as loadSiteConfig, n as buildPluginFrames, r as configHasPlugins } from "./plugins-
|
|
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
|
-
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, taskRequestProblem } from "@goodfellow-cms/core";
|
|
4
|
-
import { BUILD_DATES_DIR,
|
|
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";
|
|
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";
|
|
8
9
|
import { checkBuiltPages } from "@goodfellow-cms/core/accessibility";
|
|
10
|
+
import { CONTENT_API_DIR } from "@goodfellow-cms/core/content-api";
|
|
9
11
|
//#region src/export.ts
|
|
10
12
|
/**
|
|
11
13
|
* Writes the calendar files of the site's collections of events into the
|
|
@@ -13,7 +15,7 @@ import { checkBuiltPages } from "@goodfellow-cms/core/accessibility";
|
|
|
13
15
|
* how many it wrote.
|
|
14
16
|
*/
|
|
15
17
|
async function writeCalendarFiles(outDir, root = process.cwd()) {
|
|
16
|
-
const files = calendarFiles(shownContent(await loadSiteContent(
|
|
18
|
+
const files = calendarFiles(shownContent(await loadSiteContent(builtSiteSource(resolve(root)))));
|
|
17
19
|
for (const file of files) {
|
|
18
20
|
const target = join(outDir, file.path.slice(1));
|
|
19
21
|
await mkdir(dirname(target), { recursive: true });
|
|
@@ -26,7 +28,7 @@ async function writeCalendarFiles(outDir, root = process.cwd()) {
|
|
|
26
28
|
* build` does. Returns how many it wrote: none without the site's address.
|
|
27
29
|
*/
|
|
28
30
|
async function writeFeedFiles(outDir, root = process.cwd()) {
|
|
29
|
-
const files = feedFiles(shownContent(await loadSiteContent(
|
|
31
|
+
const files = feedFiles(shownContent(await loadSiteContent(builtSiteSource(resolve(root)))));
|
|
30
32
|
for (const file of files) {
|
|
31
33
|
const target = join(outDir, file.path.slice(1));
|
|
32
34
|
await mkdir(dirname(target), { recursive: true });
|
|
@@ -35,12 +37,38 @@ async function writeFeedFiles(outDir, root = process.cwd()) {
|
|
|
35
37
|
return files.length;
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
40
|
+
* Writes the content API (`/api/`) into the export, as `goodfellow build`
|
|
41
|
+
* does: the site's content as JSON, each page as it's shown, prepared in Node
|
|
42
|
+
* with the site's config as `goodfellow build` prepares it, so both write the
|
|
43
|
+
* same files. Returns how many pages it has, or `undefined` where the
|
|
44
|
+
* config's `api` is `false`.
|
|
45
|
+
*/
|
|
46
|
+
async function writeContentApi(outDir, root = process.cwd()) {
|
|
47
|
+
const dir = resolve(root);
|
|
48
|
+
const config = await loadSiteConfig(dir);
|
|
49
|
+
if (config.api === false) return void 0;
|
|
50
|
+
const { contentApi } = await import("@goodfellow-cms/react/server");
|
|
51
|
+
const content = shownContent(await loadSiteContent(builtSiteSource(dir)));
|
|
52
|
+
const pages = sitePages(content, todayIn(content.settings.timeZone), config.blocks);
|
|
53
|
+
const files = await contentApi(config, content, pages, { base: await exportBasePath(dir) });
|
|
54
|
+
await rm(join(outDir, CONTENT_API_DIR), {
|
|
55
|
+
recursive: true,
|
|
56
|
+
force: true
|
|
57
|
+
});
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const target = join(outDir, file.path.slice(1));
|
|
60
|
+
await mkdir(dirname(target), { recursive: true });
|
|
61
|
+
await writeFile(target, file.content);
|
|
62
|
+
}
|
|
63
|
+
return pages.length;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
38
66
|
* The folders the calendar files and feeds go in: `calendars/` and `feeds/`,
|
|
39
67
|
* and on a site in several languages, the same under each other language's
|
|
40
68
|
* code, such as `es/feeds/`.
|
|
41
69
|
*/
|
|
42
70
|
async function buildFileFolders(root = process.cwd()) {
|
|
43
|
-
const { settings } = await loadSiteContent(
|
|
71
|
+
const { settings } = await loadSiteContent(builtSiteSource(resolve(root)));
|
|
44
72
|
return siteLanguages(settings).flatMap((language) => [CALENDARS_DIR, FEEDS_DIR].map((dir) => `${language.prefix.slice(1)}${language.prefix ? "/" : ""}${dir}`));
|
|
45
73
|
}
|
|
46
74
|
/** The `basePath` the export was built with, which Next.js records in `.next/routes-manifest.json`. */
|
|
@@ -92,7 +120,7 @@ async function exists(path) {
|
|
|
92
120
|
* subfolder (`rootRobotsUrl()`).
|
|
93
121
|
*/
|
|
94
122
|
async function writeFontFiles(outDir, root = process.cwd()) {
|
|
95
|
-
const { settings } = await loadSiteContent(
|
|
123
|
+
const { settings } = await loadSiteContent(builtSiteSource(resolve(root)));
|
|
96
124
|
const fonts = await selfHostFonts(outDir, settings.theme, { base: await exportBasePath(root) });
|
|
97
125
|
const robotsElsewhere = rootRobotsUrl(settings.url);
|
|
98
126
|
return {
|
|
@@ -109,7 +137,7 @@ async function writeFontFiles(outDir, root = process.cwd()) {
|
|
|
109
137
|
*/
|
|
110
138
|
async function writeBuildRecordFile(outDir, root = process.cwd()) {
|
|
111
139
|
const dir = resolve(root);
|
|
112
|
-
const content = await loadSiteContent(
|
|
140
|
+
const content = await loadSiteContent(builtSiteSource(dir));
|
|
113
141
|
const date = await reportedDate(join(dir, ".next", BUILD_DATES_DIR));
|
|
114
142
|
return (await writeBuildRecord(outDir, {
|
|
115
143
|
root: dir,
|
|
@@ -126,7 +154,7 @@ async function writeBuildRecordFile(outDir, root = process.cwd()) {
|
|
|
126
154
|
*/
|
|
127
155
|
async function writeRedirectFiles(outDir, root = process.cwd()) {
|
|
128
156
|
const dir = resolve(root);
|
|
129
|
-
const content = shownContent(await loadSiteContent(
|
|
157
|
+
const content = shownContent(await loadSiteContent(builtSiteSource(dir)));
|
|
130
158
|
const base = await exportBasePath(dir);
|
|
131
159
|
const files = redirectFiles(content, {
|
|
132
160
|
taken: allPages(content).map((page) => page.path),
|
|
@@ -150,6 +178,41 @@ async function writeRedirectFiles(outDir, root = process.cwd()) {
|
|
|
150
178
|
};
|
|
151
179
|
}
|
|
152
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
|
+
/**
|
|
153
216
|
* For a site with an admin address (its config's `admin`, which the exported
|
|
154
217
|
* `goodfellow-site.json` carries), puts the guard first in every page of the
|
|
155
218
|
* export and adds Cloudflare Pages' header rules, as `goodfellow build` does.
|
|
@@ -162,7 +225,7 @@ async function guardExport(outDir, root = process.cwd()) {
|
|
|
162
225
|
const admin = parsed?.ok ? parsed.description.admin : void 0;
|
|
163
226
|
if (!admin) return void 0;
|
|
164
227
|
const dir = resolve(root);
|
|
165
|
-
const content = await loadSiteContent(
|
|
228
|
+
const content = await loadSiteContent(builtSiteSource(dir));
|
|
166
229
|
return guardBuiltSite(outDir, {
|
|
167
230
|
admin,
|
|
168
231
|
...parsed?.ok && parsed.description.broker && { broker: parsed.description.broker },
|
|
@@ -246,7 +309,7 @@ function withPageLanguage(html, language) {
|
|
|
246
309
|
*/
|
|
247
310
|
async function writePageLanguages(outDir, root = process.cwd()) {
|
|
248
311
|
const dir = resolve(root);
|
|
249
|
-
const { settings } = await loadSiteContent(
|
|
312
|
+
const { settings } = await loadSiteContent(builtSiteSource(dir));
|
|
250
313
|
if (!hasLanguages(settings)) return 0;
|
|
251
314
|
const base = outDir ?? join(dir, ".next/server/app");
|
|
252
315
|
const files = (await readdir(base, { recursive: true })).map((file) => file.split("\\").join("/")).filter((file) => file.endsWith(".html") && !/^(admin|_next|pagefind)\//.test(file));
|
|
@@ -314,7 +377,7 @@ function contentFile(pages, path) {
|
|
|
314
377
|
* Pages that aren't the site's, such as Next.js's own, are left out.
|
|
315
378
|
*/
|
|
316
379
|
async function checkPages(pages, root = process.cwd()) {
|
|
317
|
-
const known = allPages(shownContent(await loadSiteContent(
|
|
380
|
+
const known = allPages(shownContent(await loadSiteContent(builtSiteSource(resolve(root)))));
|
|
318
381
|
return checkBuiltPages(pages.flatMap((page) => {
|
|
319
382
|
const path = page.url.replace(/(.)\/$/, "$1");
|
|
320
383
|
const file = contentFile(known, path);
|
|
@@ -328,8 +391,8 @@ async function checkPages(pages, root = process.cwd()) {
|
|
|
328
391
|
/**
|
|
329
392
|
* Runs one of a plugin's tasks for the admin panel, as `goodfellow task`
|
|
330
393
|
* does for other sites, with the site's config loaded in Node, and writes its
|
|
331
|
-
* answer sealed to the request's key, even when the task fails.
|
|
332
|
-
*
|
|
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.
|
|
333
396
|
*/
|
|
334
397
|
async function writeTaskResult(root, request, out = TASK_RESULT_FILE) {
|
|
335
398
|
const secrets = taskSecretsFrom(process.env);
|
|
@@ -337,7 +400,10 @@ async function writeTaskResult(root, request, out = TASK_RESULT_FILE) {
|
|
|
337
400
|
if (problem) throw new Error(problem);
|
|
338
401
|
let answer;
|
|
339
402
|
try {
|
|
340
|
-
const plugin = (
|
|
403
|
+
const plugin = (await sitePluginsApart(resolve(root), {
|
|
404
|
+
module: import.meta.url,
|
|
405
|
+
export: "loadTaskConfig"
|
|
406
|
+
})).find((candidate) => pluginId(candidate.name) === request.plugin);
|
|
341
407
|
answer = plugin ? await runPluginTask({
|
|
342
408
|
root: resolve(root),
|
|
343
409
|
plugin,
|
|
@@ -358,5 +424,33 @@ async function writeTaskResult(root, request, out = TASK_RESULT_FILE) {
|
|
|
358
424
|
await writeFile(resolve(root, out), `${JSON.stringify(sealed)}\n`);
|
|
359
425
|
return sealed;
|
|
360
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
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Renders a page with a block from its own file as well as the site's config's,
|
|
433
|
+
* for `goodfellow-next blocks add`'s check that a block it made renders: both
|
|
434
|
+
* loaded in Node, as `goodfellow-next finish` loads the config.
|
|
435
|
+
*/
|
|
436
|
+
function blockPageRender(root) {
|
|
437
|
+
return async (page, block) => {
|
|
438
|
+
const site = resolve(root);
|
|
439
|
+
const config = await loadSiteConfig(site);
|
|
440
|
+
const entry = (await loadSiteModule(site, join(site, block.entry))).default;
|
|
441
|
+
const { createPageRenderer } = await import("@goodfellow-cms/react/server");
|
|
442
|
+
return createPageRenderer({
|
|
443
|
+
...config,
|
|
444
|
+
blocks: {
|
|
445
|
+
...config.blocks,
|
|
446
|
+
[block.name]: entry
|
|
447
|
+
}
|
|
448
|
+
})(await loadSiteContent(builtSiteSource(site)), {
|
|
449
|
+
path: "/",
|
|
450
|
+
file: "content/pages/index.json",
|
|
451
|
+
content: page
|
|
452
|
+
});
|
|
453
|
+
};
|
|
454
|
+
}
|
|
361
455
|
//#endregion
|
|
362
|
-
export { EDITOR_PAGES, NEXT_SITE_FILES, buildFileFolders, builtPages, checkPages, exportedPages, fixSegmentFiles, guardExport, writeBuildRecordFile, writeCalendarFiles, 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,6 +1,7 @@
|
|
|
1
|
-
import { ByLanguage, CurrentMenuLinks, NextImage, NextLink, PageLanguage } from "./components.js";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
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";
|
|
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";
|
|
6
7
|
import { cache } from "react";
|
|
@@ -19,12 +20,28 @@ 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
|
|
25
42
|
* content edits show on reload in development.
|
|
26
43
|
*/
|
|
27
|
-
const loadSite = cache(async (root = process.cwd()) => shownContent(await loadSiteContent(
|
|
44
|
+
const loadSite = cache(async (root = process.cwd()) => shownContent(await loadSiteContent(builtSiteSource(root))));
|
|
28
45
|
function pathOf(segments) {
|
|
29
46
|
try {
|
|
30
47
|
return normalizePagePath(`/${(segments ?? []).map((segment) => decodeURIComponent(segment)).join("/")}`);
|
|
@@ -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
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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: [
|
|
@@ -137,7 +181,7 @@ function goodfellowPages(config, options = {}) {
|
|
|
137
181
|
* editor's frames, at `app/admin/editor/page.tsx`.
|
|
138
182
|
*/
|
|
139
183
|
admin() {
|
|
140
|
-
return adminSite(config);
|
|
184
|
+
return adminSiteForCopy(adminSite(config), siteCopyFrom(process.env));
|
|
141
185
|
},
|
|
142
186
|
/** For `app/admin/layout.tsx`: the admin panel's title, kept out of search engines, with the site's icon. */
|
|
143
187
|
async adminMetadata() {
|
|
@@ -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, {
|
|
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(
|
|
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, {
|
|
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, {
|
|
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. */
|
|
@@ -271,7 +335,7 @@ function goodfellowPages(config, options = {}) {
|
|
|
271
335
|
* edit its content without its code, such as the Claude connector.
|
|
272
336
|
*/
|
|
273
337
|
siteDescription() {
|
|
274
|
-
return Response.json(describeSite(config));
|
|
338
|
+
return Response.json(describedForCopy(describeSite(config), siteCopyFrom(process.env)));
|
|
275
339
|
},
|
|
276
340
|
/** For `app/robots.ts`: lets search engines in, and points them at the sitemap once the site's address is set. */
|
|
277
341
|
async robots() {
|
|
@@ -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";
|
|
@@ -28,7 +28,8 @@ var plugins_exports = /* @__PURE__ */ __exportAll({
|
|
|
28
28
|
buildPluginFrames: () => buildPluginFrames,
|
|
29
29
|
configHasPlugins: () => configHasPlugins,
|
|
30
30
|
findConfigFile: () => findConfigFile,
|
|
31
|
-
loadSiteConfig: () => loadSiteConfig
|
|
31
|
+
loadSiteConfig: () => loadSiteConfig,
|
|
32
|
+
loadSiteModule: () => loadSiteModule
|
|
32
33
|
});
|
|
33
34
|
const CONFIG_FILES = [
|
|
34
35
|
"goodfellow.config.tsx",
|
|
@@ -55,19 +56,17 @@ async function configHasPlugins(root) {
|
|
|
55
56
|
return existsSync(index) && /\binstalledPlugin\(/.test(await readFile(index, "utf8"));
|
|
56
57
|
}
|
|
57
58
|
/**
|
|
58
|
-
*
|
|
59
|
+
* A module of the site, loaded in Node: bundled with Rolldown, with the
|
|
59
60
|
* packages it imports left for Node to load and stylesheets left out, then
|
|
60
|
-
* imported.
|
|
61
|
+
* imported.
|
|
61
62
|
*/
|
|
62
|
-
async function
|
|
63
|
-
const configFile = findConfigFile(root);
|
|
64
|
-
if (!configFile) throw new Error(`No goodfellow.config.tsx in ${root}.`);
|
|
63
|
+
async function loadSiteModule(root, input) {
|
|
65
64
|
const { build } = await import("rolldown");
|
|
66
65
|
const outDir = join(root, "node_modules/.goodfellow");
|
|
67
66
|
await mkdir(outDir, { recursive: true });
|
|
68
|
-
const file = join(outDir, `
|
|
67
|
+
const file = join(outDir, `module-${process.pid}-${Date.now()}.mjs`);
|
|
69
68
|
await build({
|
|
70
|
-
input
|
|
69
|
+
input,
|
|
71
70
|
cwd: root,
|
|
72
71
|
platform: "node",
|
|
73
72
|
tsconfig: existsSync(join(root, "tsconfig.json")) ? join(root, "tsconfig.json") : false,
|
|
@@ -91,11 +90,17 @@ async function loadSiteConfig(root) {
|
|
|
91
90
|
}
|
|
92
91
|
});
|
|
93
92
|
try {
|
|
94
|
-
return
|
|
93
|
+
return await import(pathToFileURL(file).href);
|
|
95
94
|
} finally {
|
|
96
95
|
await rm(file, { force: true });
|
|
97
96
|
}
|
|
98
97
|
}
|
|
98
|
+
/** The site's config, loaded in Node with `loadSiteModule()`. Only what plugins and checks need is read from it. */
|
|
99
|
+
async function loadSiteConfig(root) {
|
|
100
|
+
const configFile = findConfigFile(root);
|
|
101
|
+
if (!configFile) throw new Error(`No goodfellow.config.tsx in ${root}.`);
|
|
102
|
+
return (await loadSiteModule(root, configFile)).default;
|
|
103
|
+
}
|
|
99
104
|
/** A plugin's admin module as the bundler imports it: a package's export, or a path from the site's root. */
|
|
100
105
|
function adminModule(root, specifier) {
|
|
101
106
|
return specifier.startsWith("./") || specifier.startsWith("../") ? resolve(root, specifier) : specifier;
|
|
@@ -119,7 +124,9 @@ async function buildPluginFrame(plugin, { root, dir, watch }) {
|
|
|
119
124
|
permissions: plugin.permissions ?? {},
|
|
120
125
|
scripts: ["plugin.js"],
|
|
121
126
|
stylesheets: ["plugin.css"],
|
|
122
|
-
licenses: LICENSES_FILE
|
|
127
|
+
licenses: LICENSES_FILE,
|
|
128
|
+
editor: Boolean(plugin.editor),
|
|
129
|
+
...plugin.editor && { media: await mediaStorageOrigins(root) }
|
|
123
130
|
}));
|
|
124
131
|
};
|
|
125
132
|
const options = {
|
|
@@ -188,4 +195,4 @@ async function buildPluginFrames(plugins, { root, outDir }) {
|
|
|
188
195
|
return built;
|
|
189
196
|
}
|
|
190
197
|
//#endregion
|
|
191
|
-
export {
|
|
198
|
+
export { loadSiteModule as a, loadSiteConfig as i, buildPluginFrames as n, plugins_exports as o, configHasPlugins as r, buildPluginFrame as t };
|
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.
|
|
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.
|
|
67
|
-
"@goodfellow-cms/core": "0.
|
|
68
|
-
"@goodfellow-cms/react": "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.
|
|
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.
|
|
80
|
-
"@goodfellow-cms/bitbucket": "0.
|
|
81
|
-
"@goodfellow-cms/github": "0.
|
|
82
|
-
"@goodfellow-cms/gitlab": "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",
|