@goodfellow-cms/next 0.8.0 → 0.10.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 +205 -13
- package/dist/admin.js +1 -1
- package/dist/components.d.ts +7 -0
- package/dist/components.js +20 -2
- package/dist/config.d.ts +8 -0
- package/dist/config.js +33 -6
- package/dist/export.d.ts +17 -9
- package/dist/export.js +37 -15
- package/dist/{image-sizes-mark-Ctz9p44g.js → image-sizes-mark-BOE7ObB9.js} +7 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +57 -21
- package/dist/{site-files-BeLnLxCl.js → site-files-Cmu3ovIs.js} +30 -16
- package/package.json +14 -10
package/bin/goodfellow-next.js
CHANGED
|
@@ -3,42 +3,72 @@
|
|
|
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.
|
|
12
|
-
// `goodfellow-next
|
|
13
|
+
// `goodfellow-next import markdown <folder> [--dry-run]` imports a folder of Markdown files, as `goodfellow import` does.
|
|
14
|
+
// `goodfellow-next export wordpress [file]` writes the site as a WordPress export file, and `goodfellow-next export
|
|
15
|
+
// markdown [folder] [--scheduled] [--links site|files]` as a folder of Markdown files, as `goodfellow export` does.
|
|
16
|
+
// `goodfellow-next export site [file]` and `goodfellow-next import site <zip or folder>` move content between
|
|
17
|
+
// Goodfellow sites, as `goodfellow export site` and `goodfellow import site` do.
|
|
13
18
|
// `goodfellow-next blocks add <item> [--registry <folder>] [--native <app>]` makes a block of a shadcn item, as
|
|
14
19
|
// `goodfellow blocks add` does, and with --native its version for an Expo app.
|
|
15
20
|
// `goodfellow-next admin add <address>|check|remove` sets the admin address, and `goodfellow-next broker use <address>|remove`
|
|
16
|
-
// the token broker,
|
|
21
|
+
// the token broker, and `goodfellow-next sign-in service <address>|client-id <id>|remove` how editors sign in, in
|
|
22
|
+
// goodfellow.config.tsx, as `goodfellow admin`, `goodfellow broker` and `goodfellow sign-in` do.
|
|
17
23
|
import { existsSync, readFileSync } from "node:fs";
|
|
18
24
|
import { rm } from "node:fs/promises";
|
|
19
|
-
import { relative, resolve } from "node:path";
|
|
25
|
+
import { join, relative, resolve } from "node:path";
|
|
20
26
|
import { parseArgs } from "node:util";
|
|
21
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
buildNotes,
|
|
29
|
+
CLOUDFLARE_REDIRECTS_LIMIT,
|
|
30
|
+
configAdmin,
|
|
31
|
+
formatUpdateResult,
|
|
32
|
+
SiteImportError,
|
|
33
|
+
} from "@goodfellow-cms/core";
|
|
22
34
|
import { reportSiteProblems } from "@goodfellow-cms/core/accessibility";
|
|
23
35
|
import {
|
|
24
36
|
AdminSetupError,
|
|
25
37
|
BrokerSetupError,
|
|
26
38
|
ContentRepositoryError,
|
|
39
|
+
checkEarlyTheme,
|
|
27
40
|
checkSiteAdminAddress,
|
|
28
41
|
contentRepositoryProblem,
|
|
42
|
+
exportMarkdown,
|
|
43
|
+
exportSite,
|
|
29
44
|
exportWordPress,
|
|
30
45
|
formatAdminResult,
|
|
46
|
+
formatAdminText,
|
|
47
|
+
formatMarkdownExport,
|
|
48
|
+
formatMarkdownImport,
|
|
49
|
+
formatSiteExport,
|
|
50
|
+
formatSiteImport,
|
|
31
51
|
formatWordPressExport,
|
|
32
52
|
formatWordPressImport,
|
|
53
|
+
importMarkdown,
|
|
54
|
+
importSite,
|
|
33
55
|
importWordPress,
|
|
56
|
+
MarkdownExportError,
|
|
57
|
+
MarkdownFolderError,
|
|
58
|
+
markdownFolderChoices,
|
|
34
59
|
nightlyCheck,
|
|
35
60
|
reportDue,
|
|
36
61
|
reportNightly,
|
|
62
|
+
SignInSetupError,
|
|
63
|
+
SiteTransferError,
|
|
37
64
|
scheduleDue,
|
|
38
65
|
setAdminAddress,
|
|
39
66
|
setBrokerAddress,
|
|
67
|
+
setSignIn,
|
|
40
68
|
setUpContentRepository,
|
|
41
69
|
update,
|
|
70
|
+
writeAdminText,
|
|
71
|
+
writeTaskDescription,
|
|
42
72
|
} from "@goodfellow-cms/core/node";
|
|
43
73
|
import { writePagesSearchIndex, writeSearchIndex } from "@goodfellow-cms/core/search-index";
|
|
44
74
|
import { addShadcnBlock, blockWorks, formatShadcnBlock, ShadcnBlockError } from "@goodfellow-cms/core/shadcn";
|
|
@@ -49,9 +79,12 @@ import {
|
|
|
49
79
|
builtPages,
|
|
50
80
|
checkPages,
|
|
51
81
|
exportedPages,
|
|
82
|
+
exportTrailingSlash,
|
|
52
83
|
fixSegmentFiles,
|
|
53
84
|
guardExport,
|
|
85
|
+
loadTaskConfig,
|
|
54
86
|
NEXT_SITE_FILES,
|
|
87
|
+
nextDistDir,
|
|
55
88
|
writeBuildRecordFile,
|
|
56
89
|
writeCalendarFiles,
|
|
57
90
|
writeContentApi,
|
|
@@ -71,6 +104,19 @@ const CONFIG_FILES = ["goodfellow.config.tsx", "goodfellow.config.ts", "goodfell
|
|
|
71
104
|
// A full-stack site (withGoodfellow(config, { server: true })): Next.js serves its pages and sends redirects
|
|
72
105
|
// itself, so the calendar files, feeds and the search index are written into public/, which `next start` and hosts
|
|
73
106
|
// serve as static files, as they are for an export.
|
|
107
|
+
// A theme installed before it was three days old is used only once its provenance checks out, whatever the
|
|
108
|
+
// admin panel said, since a browser can lie.
|
|
109
|
+
if (command === "finish") {
|
|
110
|
+
// Pages' full addresses end as Next.js served the pages, unless where the site is built says otherwise.
|
|
111
|
+
process.env.GOODFELLOW_TRAILING_SLASH ||= String(await exportTrailingSlash());
|
|
112
|
+
try {
|
|
113
|
+
const theme = await checkEarlyTheme(process.cwd());
|
|
114
|
+
if (theme) console.log(`${theme.name} ${theme.release} was installed early: its provenance checks out.`);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(error instanceof Error ? error.message : error);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
74
120
|
if (command === "finish" && dir === "--server") {
|
|
75
121
|
// Next.js renders a full-stack site's pages as it serves them, so nothing can put a guard first in each.
|
|
76
122
|
const config = CONFIG_FILES.find((file) => existsSync(file));
|
|
@@ -82,13 +128,15 @@ if (command === "finish" && dir === "--server") {
|
|
|
82
128
|
}
|
|
83
129
|
for (const folder of await buildFileFolders()) await rm(`public/${folder}`, { recursive: true, force: true });
|
|
84
130
|
// The editor's frames' page, which next start serves as it was rendered.
|
|
85
|
-
await writeEditorPolicy("
|
|
131
|
+
await writeEditorPolicy(join(await nextDistDir(), "server/app"));
|
|
86
132
|
const calendars = await writeCalendarFiles("public");
|
|
87
133
|
if (calendars > 0) console.log(`Wrote ${calendars} calendar files, in public/calendars`);
|
|
88
134
|
const feeds = await writeFeedFiles("public");
|
|
89
135
|
if (feeds > 0) console.log(`Wrote ${feeds} feeds, in public/feeds`);
|
|
90
136
|
const api = await writeContentApi("public");
|
|
91
137
|
if (api !== undefined) console.log(`Wrote the content API, with ${api} pages, in public/api`);
|
|
138
|
+
// The admin panel's text in the site's own languages, for the sign-in screen, which can't read the site yet.
|
|
139
|
+
for (const line of formatAdminText(await writeAdminText("public"))) console.log(line);
|
|
92
140
|
// Before the pages are read, so the search index and the checks see each page's language.
|
|
93
141
|
const languages = await writePageLanguages(undefined);
|
|
94
142
|
if (languages > 0) console.log(`Marked ${languages} pages with their language`);
|
|
@@ -119,8 +167,20 @@ if (command === "update") {
|
|
|
119
167
|
process.exit(result.state === "failed" ? 1 : 0);
|
|
120
168
|
}
|
|
121
169
|
if (command === "task") {
|
|
122
|
-
const { values } = parseArgs({
|
|
170
|
+
const { values } = parseArgs({
|
|
171
|
+
args: process.argv.slice(3),
|
|
172
|
+
options: { out: { type: "string" }, describe: { type: "boolean" } },
|
|
173
|
+
});
|
|
123
174
|
const env = process.env;
|
|
175
|
+
if (values.describe) {
|
|
176
|
+
await writeTaskDescription(
|
|
177
|
+
".",
|
|
178
|
+
{ module: new URL("../dist/export.js", import.meta.url).href, export: "loadTaskConfig" },
|
|
179
|
+
{ plugin: env.GOODFELLOW_TASK_PLUGIN ?? "", task: env.GOODFELLOW_TASK_NAME ?? "" },
|
|
180
|
+
values.out,
|
|
181
|
+
);
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
124
184
|
await writeTaskResult(
|
|
125
185
|
".",
|
|
126
186
|
{
|
|
@@ -173,9 +233,24 @@ if (command === "content") {
|
|
|
173
233
|
}
|
|
174
234
|
}
|
|
175
235
|
}
|
|
176
|
-
if (command === "admin" || command === "broker") {
|
|
236
|
+
if (command === "admin" || command === "broker" || command === "sign-in") {
|
|
177
237
|
const [action, argument] = process.argv.slice(3);
|
|
178
238
|
try {
|
|
239
|
+
if (
|
|
240
|
+
command === "sign-in" &&
|
|
241
|
+
(((action === "service" || action === "client-id") && argument) || action === "remove")
|
|
242
|
+
) {
|
|
243
|
+
const { host, value } = await setSignIn(".", action === "remove" ? undefined : argument);
|
|
244
|
+
const name = host === "github" ? "GitHub" : host === "gitlab" ? "GitLab" : "Bitbucket";
|
|
245
|
+
console.log(
|
|
246
|
+
!value
|
|
247
|
+
? "Editors sign in with a token once the site has rebuilt with goodfellow.config.tsx."
|
|
248
|
+
: host === "github"
|
|
249
|
+
? `Editors can choose Sign in with GitHub, through ${value}, once the site has rebuilt with goodfellow.config.tsx.`
|
|
250
|
+
: `Editors can choose Sign in with ${name} once the site has rebuilt with goodfellow.config.tsx.`,
|
|
251
|
+
);
|
|
252
|
+
process.exit(0);
|
|
253
|
+
}
|
|
179
254
|
if (command === "admin" && ((action === "add" && argument) || action === "check" || action === "remove")) {
|
|
180
255
|
const result =
|
|
181
256
|
action === "check"
|
|
@@ -195,7 +270,9 @@ if (command === "admin" || command === "broker") {
|
|
|
195
270
|
process.exit(0);
|
|
196
271
|
}
|
|
197
272
|
} catch (error) {
|
|
198
|
-
if (!(error instanceof AdminSetupError || error instanceof BrokerSetupError))
|
|
273
|
+
if (!(error instanceof AdminSetupError || error instanceof BrokerSetupError || error instanceof SignInSetupError)) {
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
199
276
|
console.error(error.message);
|
|
200
277
|
process.exit(1);
|
|
201
278
|
}
|
|
@@ -204,10 +281,73 @@ if (command === "import") {
|
|
|
204
281
|
const { positionals, values } = parseArgs({
|
|
205
282
|
args: process.argv.slice(3),
|
|
206
283
|
allowPositionals: true,
|
|
207
|
-
options: {
|
|
284
|
+
options: {
|
|
285
|
+
"dry-run": { type: "boolean" },
|
|
286
|
+
pages: { type: "string", multiple: true },
|
|
287
|
+
collection: { type: "string", multiple: true },
|
|
288
|
+
"replace-home": { type: "boolean" },
|
|
289
|
+
"replace-pages": { type: "boolean" },
|
|
290
|
+
drafts: { type: "boolean" },
|
|
291
|
+
menu: { type: "boolean" },
|
|
292
|
+
"keep-fields": { type: "boolean" },
|
|
293
|
+
take: { type: "string" },
|
|
294
|
+
replace: { type: "boolean" },
|
|
295
|
+
menus: { type: "boolean" },
|
|
296
|
+
settings: { type: "boolean" },
|
|
297
|
+
layout: { type: "boolean" },
|
|
298
|
+
redirects: { type: "boolean" },
|
|
299
|
+
"without-missing-blocks": { type: "boolean" },
|
|
300
|
+
},
|
|
208
301
|
});
|
|
209
302
|
const [kind, from] = positionals;
|
|
210
303
|
const problem = contentRepositoryProblem(".");
|
|
304
|
+
if (kind === "site" && from && !problem) {
|
|
305
|
+
if (values.take !== undefined && values.take !== "site" && values.take !== "export") {
|
|
306
|
+
console.error('Say "--take site" or "--take export".');
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const config = await loadTaskConfig(resolve("."));
|
|
311
|
+
const result = await importSite(".", from, {
|
|
312
|
+
blocks: Object.keys(config.blocks),
|
|
313
|
+
replace: values.replace,
|
|
314
|
+
menus: values.menus,
|
|
315
|
+
settings: values.settings,
|
|
316
|
+
layout: values.layout,
|
|
317
|
+
redirects: values.redirects,
|
|
318
|
+
withoutMissingBlocks: values["without-missing-blocks"],
|
|
319
|
+
...(values.take && { take: values.take }),
|
|
320
|
+
dryRun: values["dry-run"],
|
|
321
|
+
log: (line) => console.log(line),
|
|
322
|
+
});
|
|
323
|
+
console.log(formatSiteImport(result));
|
|
324
|
+
process.exit(0);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (!(error instanceof SiteTransferError || error instanceof SiteImportError)) throw error;
|
|
327
|
+
console.error(error.message);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (kind === "markdown" && from && !problem) {
|
|
332
|
+
try {
|
|
333
|
+
const result = await importMarkdown(".", from, {
|
|
334
|
+
folders: markdownFolderChoices(values.pages, values.collection),
|
|
335
|
+
replaceHome: values["replace-home"],
|
|
336
|
+
replacePages: values["replace-pages"],
|
|
337
|
+
drafts: values.drafts,
|
|
338
|
+
menu: values.menu,
|
|
339
|
+
keepFields: values["keep-fields"],
|
|
340
|
+
dryRun: values["dry-run"],
|
|
341
|
+
log: (line) => console.log(line),
|
|
342
|
+
});
|
|
343
|
+
console.log(formatMarkdownImport(result));
|
|
344
|
+
process.exit(0);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (!(error instanceof MarkdownFolderError)) throw error;
|
|
347
|
+
console.error(error.message);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
211
351
|
if (kind === "wordpress" && from && !problem) {
|
|
212
352
|
try {
|
|
213
353
|
const result = await importWordPress(".", from, { dryRun: values["dry-run"], log: (line) => console.log(line) });
|
|
@@ -225,7 +365,12 @@ if (command === "import") {
|
|
|
225
365
|
}
|
|
226
366
|
}
|
|
227
367
|
if (command === "export") {
|
|
228
|
-
const
|
|
368
|
+
const { positionals, values } = parseArgs({
|
|
369
|
+
args: process.argv.slice(3),
|
|
370
|
+
allowPositionals: true,
|
|
371
|
+
options: { scheduled: { type: "boolean" }, links: { type: "string" } },
|
|
372
|
+
});
|
|
373
|
+
const [kind, file] = positionals;
|
|
229
374
|
const problem = contentRepositoryProblem(".");
|
|
230
375
|
if (problem) {
|
|
231
376
|
console.error(problem);
|
|
@@ -235,6 +380,34 @@ if (command === "export") {
|
|
|
235
380
|
console.log(formatWordPressExport(await exportWordPress(".", file)));
|
|
236
381
|
process.exit(0);
|
|
237
382
|
}
|
|
383
|
+
if (kind === "site") {
|
|
384
|
+
try {
|
|
385
|
+
console.log(formatSiteExport(await exportSite(".", file)));
|
|
386
|
+
process.exit(0);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (!(error instanceof SiteTransferError)) throw error;
|
|
389
|
+
console.error(error.message);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (kind === "markdown") {
|
|
394
|
+
if (values.links !== undefined && values.links !== "site" && values.links !== "files") {
|
|
395
|
+
console.error('Say "--links site" or "--links files".');
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const result = await exportMarkdown(".", file, {
|
|
400
|
+
scheduled: values.scheduled === true,
|
|
401
|
+
...(values.links && { links: values.links }),
|
|
402
|
+
});
|
|
403
|
+
console.log(formatMarkdownExport(result));
|
|
404
|
+
process.exit(0);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (!(error instanceof MarkdownExportError)) throw error;
|
|
407
|
+
console.error(error.message);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
238
411
|
}
|
|
239
412
|
if (command === "blocks") {
|
|
240
413
|
const { positionals, values } = parseArgs({
|
|
@@ -272,10 +445,17 @@ if (command !== "finish" && command !== "index") {
|
|
|
272
445
|
" goodfellow-next nightly [--mode changed|always|off] [--output <file>]",
|
|
273
446
|
" goodfellow-next content link|copy [folder]",
|
|
274
447
|
" goodfellow-next import wordpress <file or address> [--dry-run]",
|
|
448
|
+
" goodfellow-next import markdown <folder> [--dry-run] [--pages <folder>] [--collection <folder>]",
|
|
449
|
+
" [--replace-home] [--replace-pages] [--drafts] [--menu] [--keep-fields]",
|
|
275
450
|
" goodfellow-next export wordpress [file]",
|
|
451
|
+
" goodfellow-next export markdown [folder] [--scheduled] [--links site|files]",
|
|
452
|
+
" goodfellow-next export site [file]",
|
|
453
|
+
" goodfellow-next import site <zip or folder> [--dry-run] [--take site|export] [--replace]",
|
|
454
|
+
" [--menus] [--settings] [--layout] [--redirects] [--without-missing-blocks]",
|
|
276
455
|
" goodfellow-next blocks add <item> [--registry <folder>] [--native <app>]",
|
|
277
456
|
" goodfellow-next admin add <address> | check | remove",
|
|
278
457
|
" goodfellow-next broker use <address> | remove",
|
|
458
|
+
" goodfellow-next sign-in service <address> | client-id <id> | remove",
|
|
279
459
|
"",
|
|
280
460
|
"finish Gets the exported site in folder (default: out) ready to publish: puts the files",
|
|
281
461
|
" Next.js uses to load pages ahead where browsers look for them, writes calendar files",
|
|
@@ -301,9 +481,17 @@ if (command !== "finish" && command !== "index") {
|
|
|
301
481
|
" or copies them from it, as builds do.",
|
|
302
482
|
"import Brings a WordPress site's pages, posts, categories, tags, menus and media into",
|
|
303
483
|
" this site, from its export file (Tools → Export) or its address. With --dry-run,",
|
|
304
|
-
" says what it would bring across and changes nothing.",
|
|
484
|
+
" says what it would bring across and changes nothing. import markdown brings a folder",
|
|
485
|
+
" of Markdown files in: the files at the top as pages, each folder as a collection,",
|
|
486
|
+
" unless --pages or --collection names it (. for the top). import site brings another",
|
|
487
|
+
" Goodfellow site's export in: its pages, items, synced blocks and media, keeping this",
|
|
488
|
+
" site's version of what both have unless --take export; --menus, --settings, --layout",
|
|
489
|
+
" and --redirects bring those too, and --replace makes this site's content the export's.",
|
|
305
490
|
"export Writes this site as a WordPress export file (default: named after the site),",
|
|
306
|
-
" which WordPress imports with Tools → Import → WordPress
|
|
491
|
+
" which WordPress imports with Tools → Import → WordPress, or as a folder of Markdown",
|
|
492
|
+
" files, for Hugo, Jekyll, Eleventy, Astro and notes apps (--scheduled also exports what",
|
|
493
|
+
" isn't shown yet, as drafts; --links files links to the files rather than the addresses).",
|
|
494
|
+
" export site writes this site's content and media as a zip for another Goodfellow site.",
|
|
307
495
|
"blocks Installs a shadcn registry item (button, @acme/hero, or an item's address) with",
|
|
308
496
|
" shadcn's own CLI, makes a block of its component, with fields from its props, and",
|
|
309
497
|
" says what it couldn't do. With --registry, writes the block into the block registry",
|
|
@@ -314,6 +502,9 @@ if (command !== "finish" && command !== "index") {
|
|
|
314
502
|
" it, then starts the admin panel only there; remove stops using it.",
|
|
315
503
|
"broker The site's token broker in goodfellow.config.tsx: use signs editors in there once",
|
|
316
504
|
" it's online; remove stops using it.",
|
|
505
|
+
"sign-in How editors sign in, in goodfellow.config.tsx: service names a GitHub site's sign-in",
|
|
506
|
+
" service, client-id a GitLab site's OAuth application's ID or a Bitbucket site's OAuth",
|
|
507
|
+
" consumer's Key; remove takes either out.",
|
|
317
508
|
].join("\n"),
|
|
318
509
|
);
|
|
319
510
|
process.exit(1);
|
|
@@ -330,6 +521,7 @@ if (command === "finish") {
|
|
|
330
521
|
if (feeds > 0) console.log(`Wrote ${feeds} feeds`);
|
|
331
522
|
const api = await writeContentApi(dir);
|
|
332
523
|
if (api !== undefined) console.log(`Wrote the content API, with ${api} pages, in api/`);
|
|
524
|
+
for (const line of formatAdminText(await writeAdminText(dir))) console.log(line);
|
|
333
525
|
const languages = await writePageLanguages(dir);
|
|
334
526
|
if (languages > 0) console.log(`Marked ${languages} pages with their language`);
|
|
335
527
|
// Before the redirects' pages, which aren't the site's.
|
package/dist/admin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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";
|
package/dist/components.d.ts
CHANGED
|
@@ -16,6 +16,13 @@ export declare function NextImage({ src, alt, width, height, loading, ...props }
|
|
|
16
16
|
export declare function CurrentMenuLinks({ basePath }: {
|
|
17
17
|
basePath?: string;
|
|
18
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;
|
|
19
26
|
/**
|
|
20
27
|
* Opens the Search box's results with Next.js's navigation, and loads them
|
|
21
28
|
* ahead when they're pointed at or reached with the keyboard, as `next/link`
|
package/dist/components.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { CurrentPathProvider } from "@goodfellow-cms/react";
|
|
3
3
|
import { usePathname, useRouter } from "next/navigation";
|
|
4
|
-
import { useEffect } from "react";
|
|
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";
|
|
@@ -51,6 +51,24 @@ function CurrentMenuLinks({ basePath = "" }) {
|
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
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
|
+
/**
|
|
54
72
|
* Opens the Search box's results with Next.js's navigation, and loads them
|
|
55
73
|
* ahead when they're pointed at or reached with the keyboard, as `next/link`
|
|
56
74
|
* does. The Search box writes its results itself (`searchScript()`), as plain
|
|
@@ -117,4 +135,4 @@ function ByLanguage({ variants }) {
|
|
|
117
135
|
});
|
|
118
136
|
}
|
|
119
137
|
//#endregion
|
|
120
|
-
export { ByLanguage, CurrentMenuLinks, NextImage, NextLink, PageLanguage, SearchResultLinks };
|
|
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({ ... })`.
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as IMAGE_SIZES_MARK, t as
|
|
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";
|
|
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";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { createServer } from "node:http";
|
|
@@ -71,6 +71,11 @@ function startDevApi(root) {
|
|
|
71
71
|
server: "next-dev"
|
|
72
72
|
};
|
|
73
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
|
+
}
|
|
74
79
|
if (await (await editor).serve(req, res)) return;
|
|
75
80
|
if (!await handleDevApi(store, req, res, {
|
|
76
81
|
trustOrigin: isLocalOrigin,
|
|
@@ -114,6 +119,25 @@ function redirectSource(path) {
|
|
|
114
119
|
return encodeURI(path).replace(/[()[\]{}+*?:!]/g, "\\$&");
|
|
115
120
|
}
|
|
116
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
|
+
/**
|
|
117
141
|
* The site's redirects (`content/redirects.json`) for hosts that send them
|
|
118
142
|
* themselves, such as Vercel, which reads them from the build. Redirects from
|
|
119
143
|
* an address a page or a collection's item has are left out, since pages win,
|
|
@@ -130,7 +154,7 @@ async function siteRedirects(root) {
|
|
|
130
154
|
if (problems[index] || !from.ok || taken.has(from.path)) return [];
|
|
131
155
|
return [{
|
|
132
156
|
source: redirectSource(from.path),
|
|
133
|
-
destination: redirect.to
|
|
157
|
+
destination: redirectDestination(redirect.to),
|
|
134
158
|
permanent: true
|
|
135
159
|
}];
|
|
136
160
|
});
|
|
@@ -193,10 +217,13 @@ function withGoodfellow(nextConfig = {}, options = {}) {
|
|
|
193
217
|
images: imageSettings(nextConfig.images),
|
|
194
218
|
env: {
|
|
195
219
|
...nextConfig.env,
|
|
196
|
-
GOODFELLOW_BASE_PATH: basePath
|
|
220
|
+
GOODFELLOW_BASE_PATH: basePath,
|
|
221
|
+
[TRAILING_SLASH_VARIABLE]: String(nextConfig.trailingSlash ?? true)
|
|
197
222
|
}
|
|
198
223
|
};
|
|
199
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");
|
|
200
227
|
const dates = join(root, nextConfig.distDir ?? ".next", BUILD_DATES_DIR);
|
|
201
228
|
await rm(dates, {
|
|
202
229
|
recursive: true,
|
|
@@ -276,4 +303,4 @@ function withGoodfellow(nextConfig = {}, options = {}) {
|
|
|
276
303
|
};
|
|
277
304
|
}
|
|
278
305
|
//#endregion
|
|
279
|
-
export { withGoodfellow };
|
|
306
|
+
export { redirectDestination, withGoodfellow };
|
package/dist/export.d.ts
CHANGED
|
@@ -2,11 +2,6 @@ import { GoodfellowConfig, PluginTaskRequest, SelfHostedResult, SiteProblem, Tas
|
|
|
2
2
|
import { ImageSizesResult, SiteFile } from "@goodfellow-cms/core/node";
|
|
3
3
|
import { PageRender } from "@goodfellow-cms/core/shadcn";
|
|
4
4
|
//#region src/site-files.d.ts
|
|
5
|
-
/**
|
|
6
|
-
* The Next.js starter's files that `goodfellow-next update` keeps up to date
|
|
7
|
-
* where nobody has changed them, as the starter now has them. A test checks
|
|
8
|
-
* they match `templates/next`.
|
|
9
|
-
*/
|
|
10
5
|
export declare const NEXT_SITE_FILES: readonly SiteFile[];
|
|
11
6
|
//#endregion
|
|
12
7
|
//#region src/export.d.ts
|
|
@@ -35,6 +30,15 @@ export declare function writeContentApi(outDir: string, root?: string): Promise<
|
|
|
35
30
|
* code, such as `es/feeds/`.
|
|
36
31
|
*/
|
|
37
32
|
export declare function buildFileFolders(root?: string): Promise<string[]>;
|
|
33
|
+
/** The folder `next build` wrote into, as `withGoodfellow()` recorded it: the config's `distDir`, or `.next`. */
|
|
34
|
+
export declare function nextDistDir(root?: string): Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Whether the export's pages end their addresses with `/`, as Next.js's
|
|
37
|
+
* `trailingSlash` says, which Next.js records in
|
|
38
|
+
* `.next/required-server-files.json`. `true` where it can't be read, as
|
|
39
|
+
* `withGoodfellow()` sets it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function exportTrailingSlash(root?: string): Promise<boolean>;
|
|
38
42
|
/** What `writePluginFiles()` wrote and ran. */
|
|
39
43
|
export interface PluginFilesWritten {
|
|
40
44
|
/** The plugins whose admin frames were written. */
|
|
@@ -156,10 +160,14 @@ export declare function exportedPages(outDir: string): Promise<BuiltPage[]>;
|
|
|
156
160
|
*/
|
|
157
161
|
export declare function checkPages(pages: readonly BuiltPage[], root?: string): Promise<SiteProblem[]>;
|
|
158
162
|
/**
|
|
159
|
-
* Runs one of a plugin's tasks for the admin panel, as
|
|
160
|
-
* does for other sites, with the site's config loaded in
|
|
161
|
-
* answer sealed to the request's key, even when the task
|
|
162
|
-
*
|
|
163
|
+
* Runs one of a plugin's tasks for the admin panel in one job, as
|
|
164
|
+
* `goodfellow task` does for other sites, with the site's config loaded in
|
|
165
|
+
* Node, and writes its answer sealed to the request's key, even when the task
|
|
166
|
+
* fails: for sites' task setups from before 0.9. The config loads in a process
|
|
167
|
+
* of its own, without the job's secrets in its environment, but where it can
|
|
168
|
+
* read its parent's, so the starters now write the task down with
|
|
169
|
+
* `goodfellow-next task --describe` in a job with no secrets and run it with
|
|
170
|
+
* `@goodfellow-cms/core`'s `goodfellow-task`.
|
|
163
171
|
*/
|
|
164
172
|
export declare function writeTaskResult(root: string, request: PluginTaskRequest, out?: string): Promise<TaskResultFile>;
|
|
165
173
|
/** The site's config, loaded in Node, for `writeTaskResult()`'s process that loads it apart from the job's secrets. */
|
package/dist/export.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as loadSiteModule, i as loadSiteConfig, n as buildPluginFrames, r as configHasPlugins } from "./plugins-UKkfu0Uj.js";
|
|
2
|
-
import "./image-sizes-mark-
|
|
3
|
-
import { r as NEXT_SITE_FILES } from "./site-files-
|
|
2
|
+
import { t as DIST_DIR_RECORD } from "./image-sizes-mark-BOE7ObB9.js";
|
|
3
|
+
import { r as NEXT_SITE_FILES } from "./site-files-Cmu3ovIs.js";
|
|
4
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
5
|
import { BUILD_DATES_DIR, builtSiteSource, guardBuiltSite, reportedDate, runPluginBuildSteps, runPluginTask, selfHostFonts, sitePluginsApart, taskSecretsFrom, writeBuildRecord, writeImageSizes, writeSiteHeaders } from "@goodfellow-cms/core/node";
|
|
6
6
|
import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
@@ -71,16 +71,34 @@ async function buildFileFolders(root = process.cwd()) {
|
|
|
71
71
|
const { settings } = await loadSiteContent(builtSiteSource(resolve(root)));
|
|
72
72
|
return siteLanguages(settings).flatMap((language) => [CALENDARS_DIR, FEEDS_DIR].map((dir) => `${language.prefix.slice(1)}${language.prefix ? "/" : ""}${dir}`));
|
|
73
73
|
}
|
|
74
|
+
/** The folder `next build` wrote into, as `withGoodfellow()` recorded it: the config's `distDir`, or `.next`. */
|
|
75
|
+
async function nextDistDir(root = process.cwd()) {
|
|
76
|
+
const recorded = await readFile(join(root, DIST_DIR_RECORD), "utf8").catch(() => "");
|
|
77
|
+
return join(root, recorded.trim() || ".next");
|
|
78
|
+
}
|
|
74
79
|
/** The `basePath` the export was built with, which Next.js records in `.next/routes-manifest.json`. */
|
|
75
80
|
async function exportBasePath(root) {
|
|
76
81
|
try {
|
|
77
|
-
const manifest = JSON.parse(await readFile(join(root, "
|
|
82
|
+
const manifest = JSON.parse(await readFile(join(await nextDistDir(root), "routes-manifest.json"), "utf8"));
|
|
78
83
|
return typeof manifest.basePath === "string" ? manifest.basePath : "";
|
|
79
84
|
} catch {
|
|
80
85
|
return "";
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
/**
|
|
89
|
+
* Whether the export's pages end their addresses with `/`, as Next.js's
|
|
90
|
+
* `trailingSlash` says, which Next.js records in
|
|
91
|
+
* `.next/required-server-files.json`. `true` where it can't be read, as
|
|
92
|
+
* `withGoodfellow()` sets it.
|
|
93
|
+
*/
|
|
94
|
+
async function exportTrailingSlash(root = ".") {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(await readFile(join(await nextDistDir(root), "required-server-files.json"), "utf8")).config?.trailingSlash !== false;
|
|
97
|
+
} catch {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
84
102
|
* Plugins' parts that builds make: each plugin's admin frame, beside the
|
|
85
103
|
* editor's frames at `admin/plugins/<id>/`, and its build steps, each in a
|
|
86
104
|
* process of its own held to what the plugin declared. Into a static export
|
|
@@ -138,7 +156,7 @@ async function writeFontFiles(outDir, root = process.cwd()) {
|
|
|
138
156
|
async function writeBuildRecordFile(outDir, root = process.cwd()) {
|
|
139
157
|
const dir = resolve(root);
|
|
140
158
|
const content = await loadSiteContent(builtSiteSource(dir));
|
|
141
|
-
const date = await reportedDate(join(dir,
|
|
159
|
+
const date = await reportedDate(join(await nextDistDir(dir), BUILD_DATES_DIR));
|
|
142
160
|
return (await writeBuildRecord(outDir, {
|
|
143
161
|
root: dir,
|
|
144
162
|
content,
|
|
@@ -169,7 +187,7 @@ async function writeRedirectFiles(outDir, root = process.cwd()) {
|
|
|
169
187
|
await writeFile(target, file.content);
|
|
170
188
|
written.push(file);
|
|
171
189
|
}
|
|
172
|
-
const cloudflare = cloudflareRedirects(written, base);
|
|
190
|
+
const cloudflare = cloudflareRedirects(written, base, content.settings.trailingSlash !== false);
|
|
173
191
|
const redirects = join(outDir, CLOUDFLARE_REDIRECTS_PATH.slice(1));
|
|
174
192
|
if (cloudflare.file && !await exists(redirects)) await writeFile(redirects, cloudflare.file.content);
|
|
175
193
|
return {
|
|
@@ -185,14 +203,14 @@ async function writeRedirectFiles(outDir, root = process.cwd()) {
|
|
|
185
203
|
*/
|
|
186
204
|
async function writeImageFiles(outDir, root = process.cwd()) {
|
|
187
205
|
const dir = resolve(root);
|
|
188
|
-
if (!await exists(join(dir, "
|
|
206
|
+
if (!await exists(join(await nextDistDir(dir), "goodfellow-image-sizes"))) return void 0;
|
|
189
207
|
return writeImageSizes(dir, outDir, { resize: await sharpResizer() });
|
|
190
208
|
}
|
|
191
|
-
/** Makes the copies with sharp, turned as each photo's details say and without them, unless it can't load here. */
|
|
209
|
+
/** Makes the copies with sharp, as WebP, turned as each photo's details say and without them, unless it can't load here. */
|
|
192
210
|
async function sharpResizer() {
|
|
193
211
|
try {
|
|
194
212
|
const { default: sharp } = await import("sharp");
|
|
195
|
-
return async (image, width) => new Uint8Array(await sharp(image).rotate().resize({ width }).toBuffer());
|
|
213
|
+
return async (image, width) => new Uint8Array(await sharp(image).rotate().resize({ width }).webp({ quality: 80 }).toBuffer());
|
|
196
214
|
} catch {
|
|
197
215
|
return;
|
|
198
216
|
}
|
|
@@ -311,7 +329,7 @@ async function writePageLanguages(outDir, root = process.cwd()) {
|
|
|
311
329
|
const dir = resolve(root);
|
|
312
330
|
const { settings } = await loadSiteContent(builtSiteSource(dir));
|
|
313
331
|
if (!hasLanguages(settings)) return 0;
|
|
314
|
-
const base = outDir ?? join(dir, "
|
|
332
|
+
const base = outDir ?? join(await nextDistDir(dir), "server/app");
|
|
315
333
|
const files = (await readdir(base, { recursive: true })).map((file) => file.split("\\").join("/")).filter((file) => file.endsWith(".html") && !/^(admin|_next|pagefind)\//.test(file));
|
|
316
334
|
let changed = 0;
|
|
317
335
|
for (const file of files) {
|
|
@@ -331,7 +349,7 @@ async function writePageLanguages(outDir, root = process.cwd()) {
|
|
|
331
349
|
* and keeps their HTML in `.next/server/app/`. The admin panel and Next.js's own pages are left out.
|
|
332
350
|
*/
|
|
333
351
|
async function builtPages(root = process.cwd()) {
|
|
334
|
-
const next =
|
|
352
|
+
const next = await nextDistDir(resolve(root));
|
|
335
353
|
const manifest = JSON.parse(await readFile(join(next, "prerender-manifest.json"), "utf8"));
|
|
336
354
|
const trailingSlash = JSON.parse(await readFile(join(next, "required-server-files.json"), "utf8")).config?.trailingSlash === true;
|
|
337
355
|
const pages = [];
|
|
@@ -389,10 +407,14 @@ async function checkPages(pages, root = process.cwd()) {
|
|
|
389
407
|
}));
|
|
390
408
|
}
|
|
391
409
|
/**
|
|
392
|
-
* Runs one of a plugin's tasks for the admin panel, as
|
|
393
|
-
* does for other sites, with the site's config loaded in
|
|
394
|
-
* answer sealed to the request's key, even when the task
|
|
395
|
-
*
|
|
410
|
+
* Runs one of a plugin's tasks for the admin panel in one job, as
|
|
411
|
+
* `goodfellow task` does for other sites, with the site's config loaded in
|
|
412
|
+
* Node, and writes its answer sealed to the request's key, even when the task
|
|
413
|
+
* fails: for sites' task setups from before 0.9. The config loads in a process
|
|
414
|
+
* of its own, without the job's secrets in its environment, but where it can
|
|
415
|
+
* read its parent's, so the starters now write the task down with
|
|
416
|
+
* `goodfellow-next task --describe` in a job with no secrets and run it with
|
|
417
|
+
* `@goodfellow-cms/core`'s `goodfellow-task`.
|
|
396
418
|
*/
|
|
397
419
|
async function writeTaskResult(root, request, out = TASK_RESULT_FILE) {
|
|
398
420
|
const secrets = taskSecretsFrom(process.env);
|
|
@@ -453,4 +475,4 @@ function blockPageRender(root) {
|
|
|
453
475
|
};
|
|
454
476
|
}
|
|
455
477
|
//#endregion
|
|
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 };
|
|
478
|
+
export { EDITOR_PAGES, NEXT_SITE_FILES, blockPageRender, buildFileFolders, builtPages, checkPages, exportTrailingSlash, exportedPages, fixSegmentFiles, guardExport, loadTaskConfig, nextDistDir, writeBuildRecordFile, writeCalendarFiles, writeContentApi, writeEditorPolicy, writeExportHeaders, writeFeedFiles, writeFontFiles, writeImageFiles, writePageLanguages, writePluginFiles, writeRedirectFiles, writeTaskResult };
|
|
@@ -7,5 +7,11 @@
|
|
|
7
7
|
const IMAGE_SIZES_ENV = "GOODFELLOW_IMAGE_SIZES";
|
|
8
8
|
/** The file in `.next/` that tells `goodfellow-next finish` the pages list them. */
|
|
9
9
|
const IMAGE_SIZES_MARK = "goodfellow-image-sizes";
|
|
10
|
+
/**
|
|
11
|
+
* Where `withGoodfellow()` records the folder `next build` writes into (the
|
|
12
|
+
* config's `distDir`, `.next` unless it says otherwise), relative to the site,
|
|
13
|
+
* for `goodfellow-next finish`, which runs after it in a process of its own.
|
|
14
|
+
*/
|
|
15
|
+
const DIST_DIR_RECORD = "node_modules/.goodfellow/next-dist-dir";
|
|
10
16
|
//#endregion
|
|
11
|
-
export {
|
|
17
|
+
export { IMAGE_SIZES_ENV as n, IMAGE_SIZES_MARK as r, DIST_DIR_RECORD as t };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AdminSite, GoodfellowConfig, Page, SiteContent } from "@goodfellow-cms/core";
|
|
1
|
+
import { AdminSite, GoodfellowConfig, ImageSize, Page, SiteContent } from "@goodfellow-cms/core";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
3
|
import { Metadata, MetadataRoute } from "next";
|
|
4
4
|
//#region src/index.d.ts
|
|
@@ -24,8 +24,11 @@ export interface PageProps {
|
|
|
24
24
|
* content edits show on reload in development.
|
|
25
25
|
*/
|
|
26
26
|
export declare const loadSite: (root?: string) => Promise<SiteContent>;
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* A page's title, description, search engine settings and link previews, as
|
|
29
|
+
* Next.js metadata, with the shared picture's size where `media` has it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function pageMetadata(content: SiteContent, page: Page, basePath?: string, media?: Readonly<Record<string, ImageSize>>): Metadata;
|
|
29
32
|
/**
|
|
30
33
|
* Renders the site's pages in a Next.js App Router site, as Server Components.
|
|
31
34
|
* Blocks' links to the site's pages use `next/link`, and their images
|
|
@@ -75,7 +78,10 @@ export declare function goodfellowPages(config: GoodfellowConfig, options?: Good
|
|
|
75
78
|
* edit its content without its code, such as the Claude connector.
|
|
76
79
|
*/
|
|
77
80
|
siteDescription(): Response;
|
|
78
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* For `app/robots.ts`: lets search engines in, asks AI training's crawlers to stay out where Site Settings
|
|
83
|
+
* says so, and points them at the sitemap once the site's address is set (`robotsRules()`).
|
|
84
|
+
*/
|
|
79
85
|
robots(): Promise<MetadataRoute.Robots>;
|
|
80
86
|
};
|
|
81
87
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ByLanguage, CurrentMenuLinks, NextImage, NextLink, PageLanguage, SearchResultLinks } from "./components.js";
|
|
2
|
-
import "./image-sizes-mark-
|
|
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";
|
|
1
|
+
import { ByLanguage, CurrentMenuLinks, GoatCounterNavigation, NextImage, NextLink, PageLanguage, SearchResultLinks } from "./components.js";
|
|
2
|
+
import "./image-sizes-mark-BOE7ObB9.js";
|
|
3
|
+
import { BLOCK_SCRIPTS, DARK_MODE_ATTRIBUTE, IMAGE_WIDTHS, TRAILING_SLASH_VARIABLE, absoluteUrl, adminSiteForCopy, colorSchemeMeta, colorSchemeScript, consentScript, contentForPage, customFontPreloads, demoContent, describedForCopy, feedLinks, findRedirect, fontPreloads, googleFontsUrl, hasLanguages, heldCode, heldStats, languageMap, loadSiteContent, normalizePagePath, pageHead, robotsRules, shownContent, siteCopyFrom, siteLanguages, sitePages, sitemapPages, storedMediaSizes, themeToCss, todayIn, withBase, withSelfHostedFonts } from "@goodfellow-cms/core";
|
|
4
4
|
import { BUILD_DATES_ENV, builtSiteSource, fileSystemSource, fontStandIns, readMediaSizes, readStylesheet, reportDatesTo } from "@goodfellow-cms/core/node";
|
|
5
|
-
import { BodyCode, HeadCode, PageBody, PageContent, PageFooter, PageHeader, adminSite, applyPageEntry, createPuckConfigs, describeSite, prepareLayout, preparePage } from "@goodfellow-cms/react";
|
|
5
|
+
import { BodyCode, HeadCode, PageBody, PageContent, PageFooter, PageHeader, StatsCode, adminSite, applyPageEntry, createPuckConfigs, describeSite, prepareLayout, preparePage } from "@goodfellow-cms/react";
|
|
6
6
|
import { notFound, redirect } from "next/navigation";
|
|
7
7
|
import { cache } from "react";
|
|
8
8
|
import { preload } from "react-dom";
|
|
@@ -41,7 +41,10 @@ function hostedFonts(source) {
|
|
|
41
41
|
* for the length of one request, so a page and its metadata read it once, and
|
|
42
42
|
* content edits show on reload in development.
|
|
43
43
|
*/
|
|
44
|
-
const loadSite = cache(async (root = process.cwd()) => shownContent(await loadSiteContent(builtSiteSource(root
|
|
44
|
+
const loadSite = cache(async (root = process.cwd()) => shownContent(await loadSiteContent(builtSiteSource(root, {
|
|
45
|
+
...process.env,
|
|
46
|
+
[TRAILING_SLASH_VARIABLE]: process.env.GOODFELLOW_TRAILING_SLASH
|
|
47
|
+
}))));
|
|
45
48
|
function pathOf(segments) {
|
|
46
49
|
try {
|
|
47
50
|
return normalizePagePath(`/${(segments ?? []).map((segment) => decodeURIComponent(segment)).join("/")}`);
|
|
@@ -80,16 +83,25 @@ function toAbsolute(siteUrl, url) {
|
|
|
80
83
|
if (!url || !siteUrl || !url.startsWith("/") || url.startsWith("//")) return url;
|
|
81
84
|
return absoluteUrl(siteUrl, url);
|
|
82
85
|
}
|
|
83
|
-
/**
|
|
84
|
-
|
|
86
|
+
/**
|
|
87
|
+
* A page's title, description, search engine settings and link previews, as
|
|
88
|
+
* Next.js metadata, with the shared picture's size where `media` has it.
|
|
89
|
+
*/
|
|
90
|
+
function pageMetadata(content, page, basePath = configuredBasePath(), media = {}) {
|
|
85
91
|
const head = pageHead(content, page);
|
|
86
92
|
const socialImage = toAbsolute(content.settings.url, head.socialImage);
|
|
93
|
+
const socialSize = head.socialImage ? media[head.socialImage] : void 0;
|
|
87
94
|
const feeds = feedLinks(contentForPage(content, page));
|
|
88
95
|
return {
|
|
89
96
|
...content.settings.url && { metadataBase: new URL(content.settings.url) },
|
|
90
97
|
title: { absolute: head.title },
|
|
91
98
|
...head.description && { description: head.description },
|
|
92
|
-
|
|
99
|
+
robots: {
|
|
100
|
+
...head.noIndex && { index: false },
|
|
101
|
+
...head.noFollow && { follow: false },
|
|
102
|
+
...!head.noIndex && head.noSnippet && { nosnippet: true },
|
|
103
|
+
...!head.noIndex && { "max-image-preview": "large" }
|
|
104
|
+
},
|
|
93
105
|
...(head.canonicalUrl || feeds.length > 0) && { alternates: {
|
|
94
106
|
...head.canonicalUrl && { canonical: head.canonicalUrl },
|
|
95
107
|
...head.alternates && { languages: Object.fromEntries(head.alternates.map(({ language, href }) => [language, href])) },
|
|
@@ -100,12 +112,25 @@ function pageMetadata(content, page, basePath = configuredBasePath()) {
|
|
|
100
112
|
} },
|
|
101
113
|
...head.favicon && { icons: { icon: withBase(head.favicon, basePath) } },
|
|
102
114
|
openGraph: {
|
|
103
|
-
type
|
|
115
|
+
...head.type === "article" ? {
|
|
116
|
+
type: "article",
|
|
117
|
+
...head.publishedTime && { publishedTime: head.publishedTime }
|
|
118
|
+
} : { type: "website" },
|
|
119
|
+
siteName: head.siteName,
|
|
120
|
+
locale: head.locale,
|
|
104
121
|
title: head.title,
|
|
105
122
|
...head.description && { description: head.description },
|
|
106
123
|
...head.canonicalUrl && { url: head.canonicalUrl },
|
|
107
|
-
...socialImage && /^https?:\/\//.test(socialImage) && { images: [
|
|
108
|
-
|
|
124
|
+
...socialImage && /^https?:\/\//.test(socialImage) && { images: [{
|
|
125
|
+
url: socialImage,
|
|
126
|
+
...socialSize && {
|
|
127
|
+
width: socialSize.width,
|
|
128
|
+
height: socialSize.height
|
|
129
|
+
},
|
|
130
|
+
...head.socialImageAlt && { alt: head.socialImageAlt }
|
|
131
|
+
}] }
|
|
132
|
+
},
|
|
133
|
+
twitter: { card: head.card }
|
|
109
134
|
};
|
|
110
135
|
}
|
|
111
136
|
/**
|
|
@@ -135,7 +160,12 @@ function goodfellowPages(config, options = {}) {
|
|
|
135
160
|
const googleUrl = googleFontsUrl(content.settings.theme);
|
|
136
161
|
const hosted = googleUrl ? hostedFonts(googleUrl) : void 0;
|
|
137
162
|
const fontsUrl = googleUrl && hosted ? withSelfHostedFonts(googleUrl, hosted, basePath()) : googleUrl;
|
|
138
|
-
const [fontFiles, standIns] = await Promise.all([fontPreloads(content.settings.theme, readStylesheet), fontStandIns(content.settings.theme)]);
|
|
163
|
+
const [fontFiles, standIns] = await Promise.all([fontPreloads(content.settings.theme, readStylesheet), fontStandIns(content.settings.theme, { root })]);
|
|
164
|
+
for (const file of customFontPreloads(content.settings.theme)) preload(withBase(file, basePath()), {
|
|
165
|
+
as: "font",
|
|
166
|
+
type: "font/woff2",
|
|
167
|
+
crossOrigin: ""
|
|
168
|
+
});
|
|
139
169
|
for (const file of fontFiles) {
|
|
140
170
|
const href = hosted ? withSelfHostedFonts(file, hosted, basePath()) : file;
|
|
141
171
|
if (hosted && href === file) continue;
|
|
@@ -156,7 +186,7 @@ function goodfellowPages(config, options = {}) {
|
|
|
156
186
|
/* @__PURE__ */ jsx("style", {
|
|
157
187
|
href: "goodfellow-theme",
|
|
158
188
|
precedence: "default",
|
|
159
|
-
children: themeToCss(content.settings.theme, standIns)
|
|
189
|
+
children: themeToCss(content.settings.theme, standIns, { base: basePath() })
|
|
160
190
|
}),
|
|
161
191
|
fontsUrl && /* @__PURE__ */ jsx("link", {
|
|
162
192
|
rel: "stylesheet",
|
|
@@ -173,7 +203,8 @@ function goodfellowPages(config, options = {}) {
|
|
|
173
203
|
async generateMetadata({ params }) {
|
|
174
204
|
const content = await site();
|
|
175
205
|
const page = findPage(content, config.blocks, pathOf((await params).path));
|
|
176
|
-
|
|
206
|
+
if (!page) return {};
|
|
207
|
+
return pageMetadata(content, applyPageEntry(configs, content, page), basePath(), (await renderOptions()).media);
|
|
177
208
|
},
|
|
178
209
|
/**
|
|
179
210
|
* For `app/admin/page.tsx`: what the admin panel's page knows about the
|
|
@@ -246,10 +277,15 @@ function goodfellowPages(config, options = {}) {
|
|
|
246
277
|
held: heldCode(content.settings.consent, "head"),
|
|
247
278
|
siteUrl: content.settings.url
|
|
248
279
|
}),
|
|
280
|
+
/* @__PURE__ */ jsx(StatsCode, {
|
|
281
|
+
stats: content.settings.stats,
|
|
282
|
+
held: heldStats(content.settings.consent, content.settings.stats)
|
|
283
|
+
}),
|
|
249
284
|
header,
|
|
250
285
|
children,
|
|
251
286
|
footer,
|
|
252
287
|
/* @__PURE__ */ jsx(CurrentMenuLinks, { basePath: basePath() }),
|
|
288
|
+
content.settings.stats?.service === "goatcounter" && process.env.NODE_ENV !== "development" && /* @__PURE__ */ jsx(GoatCounterNavigation, {}),
|
|
253
289
|
/* @__PURE__ */ jsx(SearchResultLinks, { basePath: basePath() }),
|
|
254
290
|
/* @__PURE__ */ jsx(BodyCode, {
|
|
255
291
|
code: content.code?.body,
|
|
@@ -337,15 +373,15 @@ function goodfellowPages(config, options = {}) {
|
|
|
337
373
|
siteDescription() {
|
|
338
374
|
return Response.json(describedForCopy(describeSite(config), siteCopyFrom(process.env)));
|
|
339
375
|
},
|
|
340
|
-
/**
|
|
376
|
+
/**
|
|
377
|
+
* For `app/robots.ts`: lets search engines in, asks AI training's crawlers to stay out where Site Settings
|
|
378
|
+
* says so, and points them at the sitemap once the site's address is set (`robotsRules()`).
|
|
379
|
+
*/
|
|
341
380
|
async robots() {
|
|
342
|
-
const
|
|
381
|
+
const { settings } = await site();
|
|
343
382
|
return {
|
|
344
|
-
rules:
|
|
345
|
-
|
|
346
|
-
allow: "/"
|
|
347
|
-
},
|
|
348
|
-
...url && { sitemap: absoluteUrl(url, "/sitemap.xml") }
|
|
383
|
+
rules: robotsRules(settings),
|
|
384
|
+
...settings.url && { sitemap: absoluteUrl(settings.url, "/sitemap.xml") }
|
|
349
385
|
};
|
|
350
386
|
}
|
|
351
387
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
//#region src/site-files.ts
|
|
2
2
|
/** The release whose Next.js sites have the editor's sandboxed frames at `app/admin/editor/`. */
|
|
3
3
|
const FRAMES_RELEASE = "0.5.0";
|
|
4
|
+
/** The release whose Next.js sites tell Vercel not to build drafts' and plugins' tasks' branches. */
|
|
5
|
+
const VERCEL_BRANCHES_RELEASE = "0.9.0";
|
|
4
6
|
/** `app/admin/page.tsx` as every release before the frames wrote it, loading the site's config itself. */
|
|
5
7
|
const CLIENT_ADMIN_PAGE = `"use client";
|
|
6
8
|
|
|
@@ -30,21 +32,33 @@ export default function EditorPage() {
|
|
|
30
32
|
return <GoodfellowEditor config={config} />;
|
|
31
33
|
}
|
|
32
34
|
`;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
35
|
+
const NEXT_SITE_FILES = [
|
|
36
|
+
{
|
|
37
|
+
path: "app/admin/page.tsx",
|
|
38
|
+
since: FRAMES_RELEASE,
|
|
39
|
+
content: ADMIN_PAGE,
|
|
40
|
+
earlier: [CLIENT_ADMIN_PAGE]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
path: "app/admin/editor/page.tsx",
|
|
44
|
+
since: FRAMES_RELEASE,
|
|
45
|
+
content: EDITOR_PAGE,
|
|
46
|
+
earlier: []
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
path: "vercel.json",
|
|
50
|
+
since: VERCEL_BRANCHES_RELEASE,
|
|
51
|
+
content: `{
|
|
52
|
+
"git": {
|
|
53
|
+
"deploymentEnabled": {
|
|
54
|
+
"goodfellow-draft-*": false,
|
|
55
|
+
"goodfellow-task-*": false
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
`,
|
|
60
|
+
earlier: []
|
|
61
|
+
}
|
|
62
|
+
];
|
|
49
63
|
//#endregion
|
|
50
64
|
export { EDITOR_PAGE as n, NEXT_SITE_FILES as r, ADMIN_PAGE as t };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodfellow-cms/next",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Goodfellow pages and the admin panel in a Next.js site.",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Goodfellow CMS pages and the admin panel in a Next.js site.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"goodfellow",
|
|
7
7
|
"cms",
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
],
|
|
13
13
|
"homepage": "https://goodfellow-cms.github.io/goodfellow-cms/docs/nextjs",
|
|
14
14
|
"bugs": "https://github.com/goodfellow-cms/goodfellow-cms/issues",
|
|
15
|
+
"funding": {
|
|
16
|
+
"type": "github",
|
|
17
|
+
"url": "https://github.com/sponsors/goodfellow-cms"
|
|
18
|
+
},
|
|
15
19
|
"license": "MIT",
|
|
16
20
|
"repository": {
|
|
17
21
|
"type": "git",
|
|
@@ -63,24 +67,24 @@
|
|
|
63
67
|
"dist"
|
|
64
68
|
],
|
|
65
69
|
"dependencies": {
|
|
66
|
-
"@goodfellow-cms/ai": "0.
|
|
67
|
-
"@goodfellow-cms/core": "0.
|
|
68
|
-
"@goodfellow-cms/react": "0.
|
|
70
|
+
"@goodfellow-cms/ai": "0.10.0",
|
|
71
|
+
"@goodfellow-cms/core": "0.10.0",
|
|
72
|
+
"@goodfellow-cms/react": "0.10.0",
|
|
69
73
|
"rolldown": "1.2.12",
|
|
70
74
|
"sharp": "0.35.5"
|
|
71
75
|
},
|
|
72
76
|
"peerDependencies": {
|
|
73
|
-
"@goodfellow-cms/admin": "0.
|
|
77
|
+
"@goodfellow-cms/admin": "0.10.0",
|
|
74
78
|
"@puckeditor/core": "0.23.0",
|
|
75
79
|
"next": "^16.0.0",
|
|
76
80
|
"react": "^19.0.0",
|
|
77
81
|
"react-dom": "^19.0.0"
|
|
78
82
|
},
|
|
79
83
|
"devDependencies": {
|
|
80
|
-
"@goodfellow-cms/admin": "0.
|
|
81
|
-
"@goodfellow-cms/bitbucket": "0.
|
|
82
|
-
"@goodfellow-cms/github": "0.
|
|
83
|
-
"@goodfellow-cms/gitlab": "0.
|
|
84
|
+
"@goodfellow-cms/admin": "0.10.0",
|
|
85
|
+
"@goodfellow-cms/bitbucket": "0.10.0",
|
|
86
|
+
"@goodfellow-cms/github": "0.10.0",
|
|
87
|
+
"@goodfellow-cms/gitlab": "0.10.0",
|
|
84
88
|
"@puckeditor/core": "0.23.0",
|
|
85
89
|
"@tailwindcss/browser": "4.3.3",
|
|
86
90
|
"@types/react": "^19.2.0",
|