@goodfellow-cms/next 0.7.0 → 0.9.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 +85 -7
- package/dist/admin.js +2 -2
- package/dist/components.d.ts +18 -1
- package/dist/components.js +65 -4
- package/dist/config.d.ts +12 -2
- package/dist/config.js +106 -19
- package/dist/{dev-editor-CzE9HofK.js → dev-editor-BYUVUdPI.js} +1 -1
- package/dist/export.d.ts +34 -11
- package/dist/export.js +79 -14
- package/dist/image-sizes-mark-BOE7ObB9.js +17 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +127 -27
- package/dist/{plugins-K5i8XztM.js → plugins-UKkfu0Uj.js} +3 -2
- package/dist/server.d.ts +7 -0
- package/dist/server.js +4 -1
- package/dist/{site-files-BeLnLxCl.js → site-files-Cmu3ovIs.js} +30 -16
- package/package.json +16 -11
package/bin/goodfellow-next.js
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
// writes the calendar files of collections of events, collections' feeds, the pages at redirects' old addresses and Cloudflare Pages' _redirects, and the search index for sites with a search block,
|
|
4
4
|
// copies the site's Google Fonts into the export, builds plugins' admin frames and runs their build steps, starts every page with the admin address's guard for a site with one, and reports what automated checks find on the exported pages. `index` only writes the index.
|
|
5
5
|
// `goodfellow-next update` updates Goodfellow, as `goodfellow update` does for other sites.
|
|
6
|
-
// `goodfellow-next task [--out <file>]` runs one of a plugin's tasks for the admin panel,
|
|
6
|
+
// `goodfellow-next task [--describe] [--out <file>]` runs one of a plugin's tasks for the admin panel, or with
|
|
7
|
+
// --describe only writes it down for a job with no secrets, as `goodfellow task` does.
|
|
7
8
|
// `goodfellow-next due [--since <time>] [--output <file>]` says whether a scheduled change has come due, as `goodfellow due` does.
|
|
8
9
|
// `goodfellow-next nightly [--mode <mode>]` says whether tonight's build is needed, as `goodfellow nightly` does.
|
|
9
10
|
// `goodfellow-next content link|copy [folder]` puts a content repository's files in place, for a site whose
|
|
10
11
|
// content is kept in a repository of its own.
|
|
11
12
|
// `goodfellow-next import wordpress <file or address> [--dry-run]` imports a WordPress site, as `goodfellow import` does.
|
|
13
|
+
// `goodfellow-next import markdown <folder> [--dry-run]` imports a folder of Markdown files, as `goodfellow import` does.
|
|
12
14
|
// `goodfellow-next export wordpress [file]` writes the site as a WordPress export file, as `goodfellow export` does.
|
|
13
15
|
// `goodfellow-next blocks add <item> [--registry <folder>] [--native <app>]` makes a block of a shadcn item, as
|
|
14
16
|
// `goodfellow blocks add` does, and with --native its version for an Expo app.
|
|
@@ -16,7 +18,7 @@
|
|
|
16
18
|
// the token broker, in goodfellow.config.tsx, as `goodfellow admin` and `goodfellow broker` do.
|
|
17
19
|
import { existsSync, readFileSync } from "node:fs";
|
|
18
20
|
import { rm } from "node:fs/promises";
|
|
19
|
-
import { relative, resolve } from "node:path";
|
|
21
|
+
import { join, relative, resolve } from "node:path";
|
|
20
22
|
import { parseArgs } from "node:util";
|
|
21
23
|
import { buildNotes, CLOUDFLARE_REDIRECTS_LIMIT, configAdmin, formatUpdateResult } from "@goodfellow-cms/core";
|
|
22
24
|
import { reportSiteProblems } from "@goodfellow-cms/core/accessibility";
|
|
@@ -24,13 +26,19 @@ import {
|
|
|
24
26
|
AdminSetupError,
|
|
25
27
|
BrokerSetupError,
|
|
26
28
|
ContentRepositoryError,
|
|
29
|
+
checkEarlyTheme,
|
|
27
30
|
checkSiteAdminAddress,
|
|
28
31
|
contentRepositoryProblem,
|
|
29
32
|
exportWordPress,
|
|
30
33
|
formatAdminResult,
|
|
34
|
+
formatAdminText,
|
|
35
|
+
formatMarkdownImport,
|
|
31
36
|
formatWordPressExport,
|
|
32
37
|
formatWordPressImport,
|
|
38
|
+
importMarkdown,
|
|
33
39
|
importWordPress,
|
|
40
|
+
MarkdownFolderError,
|
|
41
|
+
markdownFolderChoices,
|
|
34
42
|
nightlyCheck,
|
|
35
43
|
reportDue,
|
|
36
44
|
reportNightly,
|
|
@@ -39,6 +47,8 @@ import {
|
|
|
39
47
|
setBrokerAddress,
|
|
40
48
|
setUpContentRepository,
|
|
41
49
|
update,
|
|
50
|
+
writeAdminText,
|
|
51
|
+
writeTaskDescription,
|
|
42
52
|
} from "@goodfellow-cms/core/node";
|
|
43
53
|
import { writePagesSearchIndex, writeSearchIndex } from "@goodfellow-cms/core/search-index";
|
|
44
54
|
import { addShadcnBlock, blockWorks, formatShadcnBlock, ShadcnBlockError } from "@goodfellow-cms/core/shadcn";
|
|
@@ -49,15 +59,19 @@ import {
|
|
|
49
59
|
builtPages,
|
|
50
60
|
checkPages,
|
|
51
61
|
exportedPages,
|
|
62
|
+
exportTrailingSlash,
|
|
52
63
|
fixSegmentFiles,
|
|
53
64
|
guardExport,
|
|
54
65
|
NEXT_SITE_FILES,
|
|
66
|
+
nextDistDir,
|
|
55
67
|
writeBuildRecordFile,
|
|
56
68
|
writeCalendarFiles,
|
|
57
69
|
writeContentApi,
|
|
58
70
|
writeEditorPolicy,
|
|
71
|
+
writeExportHeaders,
|
|
59
72
|
writeFeedFiles,
|
|
60
73
|
writeFontFiles,
|
|
74
|
+
writeImageFiles,
|
|
61
75
|
writePageLanguages,
|
|
62
76
|
writePluginFiles,
|
|
63
77
|
writeRedirectFiles,
|
|
@@ -69,6 +83,19 @@ const CONFIG_FILES = ["goodfellow.config.tsx", "goodfellow.config.ts", "goodfell
|
|
|
69
83
|
// A full-stack site (withGoodfellow(config, { server: true })): Next.js serves its pages and sends redirects
|
|
70
84
|
// itself, so the calendar files, feeds and the search index are written into public/, which `next start` and hosts
|
|
71
85
|
// serve as static files, as they are for an export.
|
|
86
|
+
// A theme installed before it was three days old is used only once its provenance checks out, whatever the
|
|
87
|
+
// admin panel said, since a browser can lie.
|
|
88
|
+
if (command === "finish") {
|
|
89
|
+
// Pages' full addresses end as Next.js served the pages, unless where the site is built says otherwise.
|
|
90
|
+
process.env.GOODFELLOW_TRAILING_SLASH ||= String(await exportTrailingSlash());
|
|
91
|
+
try {
|
|
92
|
+
const theme = await checkEarlyTheme(process.cwd());
|
|
93
|
+
if (theme) console.log(`${theme.name} ${theme.release} was installed early: its provenance checks out.`);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(error instanceof Error ? error.message : error);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
72
99
|
if (command === "finish" && dir === "--server") {
|
|
73
100
|
// Next.js renders a full-stack site's pages as it serves them, so nothing can put a guard first in each.
|
|
74
101
|
const config = CONFIG_FILES.find((file) => existsSync(file));
|
|
@@ -80,13 +107,15 @@ if (command === "finish" && dir === "--server") {
|
|
|
80
107
|
}
|
|
81
108
|
for (const folder of await buildFileFolders()) await rm(`public/${folder}`, { recursive: true, force: true });
|
|
82
109
|
// The editor's frames' page, which next start serves as it was rendered.
|
|
83
|
-
await writeEditorPolicy("
|
|
110
|
+
await writeEditorPolicy(join(await nextDistDir(), "server/app"));
|
|
84
111
|
const calendars = await writeCalendarFiles("public");
|
|
85
112
|
if (calendars > 0) console.log(`Wrote ${calendars} calendar files, in public/calendars`);
|
|
86
113
|
const feeds = await writeFeedFiles("public");
|
|
87
114
|
if (feeds > 0) console.log(`Wrote ${feeds} feeds, in public/feeds`);
|
|
88
115
|
const api = await writeContentApi("public");
|
|
89
116
|
if (api !== undefined) console.log(`Wrote the content API, with ${api} pages, in public/api`);
|
|
117
|
+
// The admin panel's text in the site's own languages, for the sign-in screen, which can't read the site yet.
|
|
118
|
+
for (const line of formatAdminText(await writeAdminText("public"))) console.log(line);
|
|
90
119
|
// Before the pages are read, so the search index and the checks see each page's language.
|
|
91
120
|
const languages = await writePageLanguages(undefined);
|
|
92
121
|
if (languages > 0) console.log(`Marked ${languages} pages with their language`);
|
|
@@ -117,8 +146,20 @@ if (command === "update") {
|
|
|
117
146
|
process.exit(result.state === "failed" ? 1 : 0);
|
|
118
147
|
}
|
|
119
148
|
if (command === "task") {
|
|
120
|
-
const { values } = parseArgs({
|
|
149
|
+
const { values } = parseArgs({
|
|
150
|
+
args: process.argv.slice(3),
|
|
151
|
+
options: { out: { type: "string" }, describe: { type: "boolean" } },
|
|
152
|
+
});
|
|
121
153
|
const env = process.env;
|
|
154
|
+
if (values.describe) {
|
|
155
|
+
await writeTaskDescription(
|
|
156
|
+
".",
|
|
157
|
+
{ module: new URL("../dist/export.js", import.meta.url).href, export: "loadTaskConfig" },
|
|
158
|
+
{ plugin: env.GOODFELLOW_TASK_PLUGIN ?? "", task: env.GOODFELLOW_TASK_NAME ?? "" },
|
|
159
|
+
values.out,
|
|
160
|
+
);
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
122
163
|
await writeTaskResult(
|
|
123
164
|
".",
|
|
124
165
|
{
|
|
@@ -202,10 +243,39 @@ if (command === "import") {
|
|
|
202
243
|
const { positionals, values } = parseArgs({
|
|
203
244
|
args: process.argv.slice(3),
|
|
204
245
|
allowPositionals: true,
|
|
205
|
-
options: {
|
|
246
|
+
options: {
|
|
247
|
+
"dry-run": { type: "boolean" },
|
|
248
|
+
pages: { type: "string", multiple: true },
|
|
249
|
+
collection: { type: "string", multiple: true },
|
|
250
|
+
"replace-home": { type: "boolean" },
|
|
251
|
+
"replace-pages": { type: "boolean" },
|
|
252
|
+
drafts: { type: "boolean" },
|
|
253
|
+
menu: { type: "boolean" },
|
|
254
|
+
"keep-fields": { type: "boolean" },
|
|
255
|
+
},
|
|
206
256
|
});
|
|
207
257
|
const [kind, from] = positionals;
|
|
208
258
|
const problem = contentRepositoryProblem(".");
|
|
259
|
+
if (kind === "markdown" && from && !problem) {
|
|
260
|
+
try {
|
|
261
|
+
const result = await importMarkdown(".", from, {
|
|
262
|
+
folders: markdownFolderChoices(values.pages, values.collection),
|
|
263
|
+
replaceHome: values["replace-home"],
|
|
264
|
+
replacePages: values["replace-pages"],
|
|
265
|
+
drafts: values.drafts,
|
|
266
|
+
menu: values.menu,
|
|
267
|
+
keepFields: values["keep-fields"],
|
|
268
|
+
dryRun: values["dry-run"],
|
|
269
|
+
log: (line) => console.log(line),
|
|
270
|
+
});
|
|
271
|
+
console.log(formatMarkdownImport(result));
|
|
272
|
+
process.exit(0);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (!(error instanceof MarkdownFolderError)) throw error;
|
|
275
|
+
console.error(error.message);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
209
279
|
if (kind === "wordpress" && from && !problem) {
|
|
210
280
|
try {
|
|
211
281
|
const result = await importWordPress(".", from, { dryRun: values["dry-run"], log: (line) => console.log(line) });
|
|
@@ -270,6 +340,8 @@ if (command !== "finish" && command !== "index") {
|
|
|
270
340
|
" goodfellow-next nightly [--mode changed|always|off] [--output <file>]",
|
|
271
341
|
" goodfellow-next content link|copy [folder]",
|
|
272
342
|
" goodfellow-next import wordpress <file or address> [--dry-run]",
|
|
343
|
+
" goodfellow-next import markdown <folder> [--dry-run] [--pages <folder>] [--collection <folder>]",
|
|
344
|
+
" [--replace-home] [--replace-pages] [--drafts] [--menu] [--keep-fields]",
|
|
273
345
|
" goodfellow-next export wordpress [file]",
|
|
274
346
|
" goodfellow-next blocks add <item> [--registry <folder>] [--native <app>]",
|
|
275
347
|
" goodfellow-next admin add <address> | check | remove",
|
|
@@ -299,7 +371,9 @@ if (command !== "finish" && command !== "index") {
|
|
|
299
371
|
" or copies them from it, as builds do.",
|
|
300
372
|
"import Brings a WordPress site's pages, posts, categories, tags, menus and media into",
|
|
301
373
|
" this site, from its export file (Tools → Export) or its address. With --dry-run,",
|
|
302
|
-
" says what it would bring across and changes nothing.",
|
|
374
|
+
" says what it would bring across and changes nothing. import markdown brings a folder",
|
|
375
|
+
" of Markdown files in: the files at the top as pages, each folder as a collection,",
|
|
376
|
+
" unless --pages or --collection names it (. for the top).",
|
|
303
377
|
"export Writes this site as a WordPress export file (default: named after the site),",
|
|
304
378
|
" which WordPress imports with Tools → Import → WordPress.",
|
|
305
379
|
"blocks Installs a shadcn registry item (button, @acme/hero, or an item's address) with",
|
|
@@ -328,6 +402,7 @@ if (command === "finish") {
|
|
|
328
402
|
if (feeds > 0) console.log(`Wrote ${feeds} feeds`);
|
|
329
403
|
const api = await writeContentApi(dir);
|
|
330
404
|
if (api !== undefined) console.log(`Wrote the content API, with ${api} pages, in api/`);
|
|
405
|
+
for (const line of formatAdminText(await writeAdminText(dir))) console.log(line);
|
|
331
406
|
const languages = await writePageLanguages(dir);
|
|
332
407
|
if (languages > 0) console.log(`Marked ${languages} pages with their language`);
|
|
333
408
|
// Before the redirects' pages, which aren't the site's.
|
|
@@ -339,7 +414,10 @@ if (command === "finish") {
|
|
|
339
414
|
`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
415
|
);
|
|
341
416
|
}
|
|
342
|
-
|
|
417
|
+
// Cloudflare Pages' header rules, after the site's own public/_headers.
|
|
418
|
+
await writeExportHeaders(dir);
|
|
419
|
+
const images = await writeImageFiles(dir);
|
|
420
|
+
for (const line of buildNotes({ ...(await writeFontFiles(dir)), ...(images && { images }) })) console.log(line);
|
|
343
421
|
// Plugins' admin frames and build steps, once Goodfellow has written the rest, before the index and the guards.
|
|
344
422
|
const plugins = await writePluginFiles(dir);
|
|
345
423
|
if (plugins.frames.length > 0) console.log(`Wrote the admin frames of ${plugins.frames.join(", ")}`);
|
package/dist/admin.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { n as EDITOR_PAGE, t as ADMIN_PAGE } from "./site-files-
|
|
2
|
+
import { n as EDITOR_PAGE, t as ADMIN_PAGE } from "./site-files-Cmu3ovIs.js";
|
|
3
3
|
import { ADMIN_POLICY, DEV_EDITOR_PREFIX } from "@goodfellow-cms/core";
|
|
4
4
|
import { useEffect, useState } from "react";
|
|
5
5
|
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,28 @@ 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
|
+
* Counts each page visitors move to with GoatCounter, whose script counts only
|
|
21
|
+
* the page it loads on. Plausible's and Umami's scripts follow Next.js's
|
|
22
|
+
* navigations themselves. Does nothing until the script has loaded, which on a
|
|
23
|
+
* site that asks visitors first is once they allow statistics.
|
|
24
|
+
*/
|
|
25
|
+
export declare function GoatCounterNavigation(): null;
|
|
26
|
+
/**
|
|
27
|
+
* Opens the Search box's results with Next.js's navigation, and loads them
|
|
28
|
+
* ahead when they're pointed at or reached with the keyboard, as `next/link`
|
|
29
|
+
* does. The Search box writes its results itself (`searchScript()`), as plain
|
|
30
|
+
* links, so pages built with `goodfellow build` need no React for them.
|
|
31
|
+
*/
|
|
32
|
+
export declare function SearchResultLinks({ basePath }: {
|
|
33
|
+
basePath?: string;
|
|
34
|
+
}): null;
|
|
18
35
|
/**
|
|
19
36
|
* On a site in several languages, says the page's language on `<html>`, which
|
|
20
37
|
* the root layout shared by every page can't. `goodfellow-next finish` writes
|
package/dist/components.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { CurrentPathProvider } from "@goodfellow-cms/react";
|
|
3
|
-
import { usePathname } from "next/navigation";
|
|
4
|
-
import { useEffect } from "react";
|
|
3
|
+
import { usePathname, useRouter } from "next/navigation";
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
5
|
import Image from "next/image";
|
|
6
6
|
import Link from "next/link";
|
|
7
7
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -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,70 @@ 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
|
+
* Counts each page visitors move to with GoatCounter, whose script counts only
|
|
55
|
+
* the page it loads on. Plausible's and Umami's scripts follow Next.js's
|
|
56
|
+
* navigations themselves. Does nothing until the script has loaded, which on a
|
|
57
|
+
* site that asks visitors first is once they allow statistics.
|
|
58
|
+
*/
|
|
59
|
+
function GoatCounterNavigation() {
|
|
60
|
+
const pathname = usePathname();
|
|
61
|
+
const first = useRef(true);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (first.current) {
|
|
64
|
+
first.current = false;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
window.goatcounter?.count?.({ path: location.pathname + location.search });
|
|
68
|
+
}, [pathname]);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Opens the Search box's results with Next.js's navigation, and loads them
|
|
73
|
+
* ahead when they're pointed at or reached with the keyboard, as `next/link`
|
|
74
|
+
* does. The Search box writes its results itself (`searchScript()`), as plain
|
|
75
|
+
* links, so pages built with `goodfellow build` need no React for them.
|
|
76
|
+
*/
|
|
77
|
+
function SearchResultLinks({ basePath = "" }) {
|
|
78
|
+
const router = useRouter();
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const prefix = trimSlash(basePath) === "/" ? "" : trimSlash(basePath);
|
|
81
|
+
/** A result's address within the site, without the base path, which Next.js's router adds. */
|
|
82
|
+
const target = (event) => {
|
|
83
|
+
const link = event.target?.closest?.("[data-gf-search-box] a[href]");
|
|
84
|
+
if (!(link instanceof HTMLAnchorElement) || link.target && link.target !== "_self") return void 0;
|
|
85
|
+
const url = new URL(link.href, location.href);
|
|
86
|
+
if (url.origin !== location.origin) return void 0;
|
|
87
|
+
if (prefix && url.pathname !== prefix && !url.pathname.startsWith(`${prefix}/`)) return void 0;
|
|
88
|
+
return `${url.pathname.slice(prefix.length) || "/"}${url.search}${url.hash}`;
|
|
89
|
+
};
|
|
90
|
+
const open = (event) => {
|
|
91
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
92
|
+
const to = target(event);
|
|
93
|
+
if (!to) return;
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
router.push(to);
|
|
96
|
+
};
|
|
97
|
+
const prefetch = (event) => {
|
|
98
|
+
const to = target(event);
|
|
99
|
+
if (to) router.prefetch(to);
|
|
100
|
+
};
|
|
101
|
+
document.addEventListener("click", open);
|
|
102
|
+
document.addEventListener("pointerover", prefetch);
|
|
103
|
+
document.addEventListener("focusin", prefetch);
|
|
104
|
+
return () => {
|
|
105
|
+
document.removeEventListener("click", open);
|
|
106
|
+
document.removeEventListener("pointerover", prefetch);
|
|
107
|
+
document.removeEventListener("focusin", prefetch);
|
|
108
|
+
};
|
|
109
|
+
}, [router, basePath]);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
52
113
|
* On a site in several languages, says the page's language on `<html>`, which
|
|
53
114
|
* the root layout shared by every page can't. `goodfellow-next finish` writes
|
|
54
115
|
* it into exported pages; this keeps it right as visitors move between pages.
|
|
@@ -74,4 +135,4 @@ function ByLanguage({ variants }) {
|
|
|
74
135
|
});
|
|
75
136
|
}
|
|
76
137
|
//#endregion
|
|
77
|
-
export { ByLanguage, CurrentMenuLinks, NextImage, NextLink, PageLanguage };
|
|
138
|
+
export { ByLanguage, CurrentMenuLinks, GoatCounterNavigation, NextImage, NextLink, PageLanguage, SearchResultLinks };
|
package/dist/config.d.ts
CHANGED
|
@@ -11,6 +11,14 @@ export interface GoodfellowNextOptions {
|
|
|
11
11
|
*/
|
|
12
12
|
server?: boolean;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* A redirect's target as Next.js's destination: Next.js reads `:name`, `*`,
|
|
16
|
+
* `+`, `(`, `)`, `{` and `}` in its path and hash as a pattern, which would
|
|
17
|
+
* fail the build (`/a/:b` names a value the source doesn't have), so they're
|
|
18
|
+
* percent-encoded there, which browsers and servers read as the same address.
|
|
19
|
+
* Next.js escapes its query values itself.
|
|
20
|
+
*/
|
|
21
|
+
export declare function redirectDestination(to: string): string;
|
|
14
22
|
/**
|
|
15
23
|
* Sets up Next.js for a Goodfellow site, in `next.config.ts`:
|
|
16
24
|
* `export default withGoodfellow({ ... })`.
|
|
@@ -23,8 +31,10 @@ export interface GoodfellowNextOptions {
|
|
|
23
31
|
* - Serves images as they are unless the site sets an image loader, since static files can't be optimized on request.
|
|
24
32
|
* - Adds the site's redirects (Site Settings → Redirects) ahead of the site's own, for hosts that
|
|
25
33
|
* send them, such as Vercel. Others get the pages `goodfellow-next finish` writes at old addresses.
|
|
26
|
-
* -
|
|
27
|
-
* admin panel
|
|
34
|
+
* - Adds header rules for hosts that send them, such as Vercel: the media library's SVGs
|
|
35
|
+
* sandboxed, the admin panel unframed, and, for a site with an admin address, every page
|
|
36
|
+
* there sandboxed but the admin panel's own, with redirects sending pages opened at the
|
|
37
|
+
* wrong one of the two addresses to the right one.
|
|
28
38
|
* - With `{ server: true }`, builds a full-stack site for a server instead of as static
|
|
29
39
|
* files: see `goodfellowApi()` in `@goodfellow-cms/next/server`.
|
|
30
40
|
* - 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_ENV, r as IMAGE_SIZES_MARK, t as DIST_DIR_RECORD } from "./image-sizes-mark-BOE7ObB9.js";
|
|
2
|
+
import { CALENDARS_DIR, FEEDS_DIR, TRAILING_SLASH_VARIABLE, 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, isAllowedDevHost, 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,16 +61,21 @@ 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) => {
|
|
74
|
+
const forwarded = req.headers["x-forwarded-host"];
|
|
75
|
+
if (!isAllowedDevHost(req.headers.host) || forwarded !== void 0 && !isAllowedDevHost(String(forwarded))) {
|
|
76
|
+
res.writeHead(403).end();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
54
79
|
if (await (await editor).serve(req, res)) return;
|
|
55
80
|
if (!await handleDevApi(store, req, res, {
|
|
56
81
|
trustOrigin: isLocalOrigin,
|
|
@@ -94,6 +119,25 @@ function redirectSource(path) {
|
|
|
94
119
|
return encodeURI(path).replace(/[()[\]{}+*?:!]/g, "\\$&");
|
|
95
120
|
}
|
|
96
121
|
/**
|
|
122
|
+
* A redirect's target as Next.js's destination: Next.js reads `:name`, `*`,
|
|
123
|
+
* `+`, `(`, `)`, `{` and `}` in its path and hash as a pattern, which would
|
|
124
|
+
* fail the build (`/a/:b` names a value the source doesn't have), so they're
|
|
125
|
+
* percent-encoded there, which browsers and servers read as the same address.
|
|
126
|
+
* Next.js escapes its query values itself.
|
|
127
|
+
*/
|
|
128
|
+
function redirectDestination(to) {
|
|
129
|
+
const target = to.trim();
|
|
130
|
+
const origin = /^https:\/\/[^/?#]*/i.exec(target)?.[0] ?? "";
|
|
131
|
+
const rest = target.slice(origin.length);
|
|
132
|
+
const query = rest.indexOf("?");
|
|
133
|
+
const hash = rest.indexOf("#");
|
|
134
|
+
const encode = (part) => part.replace(/[:*+(){}]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
135
|
+
const pathEnd = [query, hash].filter((index) => index >= 0).reduce((a, b) => Math.min(a, b), rest.length);
|
|
136
|
+
const search = query >= 0 && (hash < 0 || query < hash) ? rest.slice(query, hash > query ? hash : void 0) : "";
|
|
137
|
+
const fragment = hash >= 0 ? rest.slice(hash) : "";
|
|
138
|
+
return `${origin}${encode(rest.slice(0, pathEnd))}${search}${encode(fragment)}`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
97
141
|
* The site's redirects (`content/redirects.json`) for hosts that send them
|
|
98
142
|
* themselves, such as Vercel, which reads them from the build. Redirects from
|
|
99
143
|
* an address a page or a collection's item has are left out, since pages win,
|
|
@@ -110,12 +154,33 @@ async function siteRedirects(root) {
|
|
|
110
154
|
if (problems[index] || !from.ok || taken.has(from.path)) return [];
|
|
111
155
|
return [{
|
|
112
156
|
source: redirectSource(from.path),
|
|
113
|
-
destination: redirect.to
|
|
157
|
+
destination: redirectDestination(redirect.to),
|
|
114
158
|
permanent: true
|
|
115
159
|
}];
|
|
116
160
|
});
|
|
117
161
|
}
|
|
118
162
|
/**
|
|
163
|
+
* Pages opened at the wrong one of the Site address and the admin address,
|
|
164
|
+
* sent to the right one before anything loads, for hosts that send redirects,
|
|
165
|
+
* such as Vercel (`vercelAdminRedirects()`). The Site address is
|
|
166
|
+
* `content/site.json`'s own, never a copy's, which leaves the admin address
|
|
167
|
+
* out. Their addresses already have the base path, so Next.js adds none.
|
|
168
|
+
*/
|
|
169
|
+
function adminRedirects(root, basePath) {
|
|
170
|
+
const admin = siteAdmin(root);
|
|
171
|
+
if (!admin) return [];
|
|
172
|
+
let siteUrl;
|
|
173
|
+
try {
|
|
174
|
+
siteUrl = JSON.parse(readFileSync(join(root, "content/site.json"), "utf8")).url;
|
|
175
|
+
} catch {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
return vercelAdminRedirects(admin, typeof siteUrl === "string" ? siteUrl : void 0, `${basePath}/`).map((rule) => ({
|
|
179
|
+
...rule,
|
|
180
|
+
basePath: false
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
119
184
|
* Sets up Next.js for a Goodfellow site, in `next.config.ts`:
|
|
120
185
|
* `export default withGoodfellow({ ... })`.
|
|
121
186
|
*
|
|
@@ -127,8 +192,10 @@ async function siteRedirects(root) {
|
|
|
127
192
|
* - Serves images as they are unless the site sets an image loader, since static files can't be optimized on request.
|
|
128
193
|
* - Adds the site's redirects (Site Settings → Redirects) ahead of the site's own, for hosts that
|
|
129
194
|
* send them, such as Vercel. Others get the pages `goodfellow-next finish` writes at old addresses.
|
|
130
|
-
* -
|
|
131
|
-
* admin panel
|
|
195
|
+
* - Adds header rules for hosts that send them, such as Vercel: the media library's SVGs
|
|
196
|
+
* sandboxed, the admin panel unframed, and, for a site with an admin address, every page
|
|
197
|
+
* there sandboxed but the admin panel's own, with redirects sending pages opened at the
|
|
198
|
+
* wrong one of the two addresses to the right one.
|
|
132
199
|
* - With `{ server: true }`, builds a full-stack site for a server instead of as static
|
|
133
200
|
* files: see `goodfellowApi()` in `@goodfellow-cms/next/server`.
|
|
134
201
|
* - In `next dev`, runs the admin panel's local backend, so publishing saves to
|
|
@@ -150,21 +217,36 @@ function withGoodfellow(nextConfig = {}, options = {}) {
|
|
|
150
217
|
images: imageSettings(nextConfig.images),
|
|
151
218
|
env: {
|
|
152
219
|
...nextConfig.env,
|
|
153
|
-
GOODFELLOW_BASE_PATH: basePath
|
|
220
|
+
GOODFELLOW_BASE_PATH: basePath,
|
|
221
|
+
[TRAILING_SLASH_VARIABLE]: String(nextConfig.trailingSlash ?? true)
|
|
154
222
|
}
|
|
155
223
|
};
|
|
156
224
|
if (phase === PHASE_PRODUCTION_BUILD) {
|
|
225
|
+
await mkdir(dirname(join(root, DIST_DIR_RECORD)), { recursive: true });
|
|
226
|
+
await writeFile(join(root, DIST_DIR_RECORD), nextConfig.distDir ?? ".next");
|
|
157
227
|
const dates = join(root, nextConfig.distDir ?? ".next", BUILD_DATES_DIR);
|
|
158
228
|
await rm(dates, {
|
|
159
229
|
recursive: true,
|
|
160
230
|
force: true
|
|
161
231
|
});
|
|
162
232
|
process.env[BUILD_DATES_ENV] = dates;
|
|
233
|
+
const mark = join(root, nextConfig.distDir ?? ".next", IMAGE_SIZES_MARK);
|
|
234
|
+
await rm(mark, { force: true });
|
|
235
|
+
if (!options.server && config.images?.unoptimized) {
|
|
236
|
+
process.env[IMAGE_SIZES_ENV] = "1";
|
|
237
|
+
await mkdir(dirname(mark), { recursive: true });
|
|
238
|
+
await writeFile(mark, "");
|
|
239
|
+
} else delete process.env[IMAGE_SIZES_ENV];
|
|
163
240
|
}
|
|
164
241
|
if (phase !== PHASE_DEVELOPMENT_SERVER && options.server) {
|
|
165
242
|
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.");
|
|
243
|
+
const fonts = phase === PHASE_PRODUCTION_BUILD ? await hostSiteFonts(root) : void 0;
|
|
166
244
|
return {
|
|
167
245
|
...config,
|
|
246
|
+
...fonts && { env: {
|
|
247
|
+
...config.env,
|
|
248
|
+
GOODFELLOW_FONTS: JSON.stringify(fonts)
|
|
249
|
+
} },
|
|
168
250
|
images: nextConfig.images,
|
|
169
251
|
outputFileTracingIncludes: {
|
|
170
252
|
...nextConfig.outputFileTracingIncludes,
|
|
@@ -174,14 +256,19 @@ function withGoodfellow(nextConfig = {}, options = {}) {
|
|
|
174
256
|
"./public/media/**/*"
|
|
175
257
|
]
|
|
176
258
|
},
|
|
177
|
-
redirects: async () => [...await siteRedirects(root), ...await nextConfig.redirects?.() ?? []]
|
|
259
|
+
redirects: async () => [...await siteRedirects(root), ...await nextConfig.redirects?.() ?? []],
|
|
260
|
+
headers: async () => [...siteHeaders(root), ...await nextConfig.headers?.() ?? []]
|
|
178
261
|
};
|
|
179
262
|
}
|
|
180
263
|
if (phase !== PHASE_DEVELOPMENT_SERVER) return {
|
|
181
264
|
output: "export",
|
|
182
265
|
...config,
|
|
183
|
-
redirects: async () => [
|
|
184
|
-
|
|
266
|
+
redirects: async () => [
|
|
267
|
+
...adminRedirects(root, basePath),
|
|
268
|
+
...await siteRedirects(root),
|
|
269
|
+
...await nextConfig.redirects?.() ?? []
|
|
270
|
+
],
|
|
271
|
+
...(siteAdmin(root) || process.env.VERCEL) && { headers: async () => [...siteHeaders(root), ...await nextConfig.headers?.() ?? []] }
|
|
185
272
|
};
|
|
186
273
|
const port = await startDevApi(root);
|
|
187
274
|
const rewrite = {
|
|
@@ -216,4 +303,4 @@ function withGoodfellow(nextConfig = {}, options = {}) {
|
|
|
216
303
|
};
|
|
217
304
|
}
|
|
218
305
|
//#endregion
|
|
219
|
-
export { withGoodfellow };
|
|
306
|
+
export { redirectDestination, withGoodfellow };
|
|
@@ -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";
|