@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/dist/export.d.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { PluginTaskRequest, SelfHostedResult, SiteProblem, TaskResultFile } from "@goodfellow-cms/core";
|
|
2
|
-
import { SiteFile } from "@goodfellow-cms/core/node";
|
|
1
|
+
import { GoodfellowConfig, PluginTaskRequest, SelfHostedResult, SiteProblem, TaskResultFile } from "@goodfellow-cms/core";
|
|
2
|
+
import { ImageSizesResult, SiteFile } from "@goodfellow-cms/core/node";
|
|
3
3
|
import { PageRender } from "@goodfellow-cms/core/shadcn";
|
|
4
4
|
//#region src/site-files.d.ts
|
|
5
|
-
/**
|
|
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. */
|
|
@@ -84,6 +88,19 @@ export interface RedirectsWritten {
|
|
|
84
88
|
* has a page at keeps it.
|
|
85
89
|
*/
|
|
86
90
|
export declare function writeRedirectFiles(outDir: string, root?: string): Promise<RedirectsWritten>;
|
|
91
|
+
/**
|
|
92
|
+
* Writes the smaller copies of the media library's photos that the export's
|
|
93
|
+
* pages list in their `srcset` (`writeImageSizes()`), with sharp, where
|
|
94
|
+
* `next build` said they list them (`IMAGE_SIZES_MARK`): a static export
|
|
95
|
+
* whose images Next.js doesn't optimize. `undefined` where they don't.
|
|
96
|
+
*/
|
|
97
|
+
export declare function writeImageFiles(outDir: string, root?: string): Promise<ImageSizesResult | undefined>;
|
|
98
|
+
/**
|
|
99
|
+
* Adds Cloudflare Pages' header rules for every site to the export's
|
|
100
|
+
* `_headers`, as `goodfellow build` does (`writeSiteHeaders()`), from the
|
|
101
|
+
* admin address and broker the exported `goodfellow-site.json` names.
|
|
102
|
+
*/
|
|
103
|
+
export declare function writeExportHeaders(outDir: string, root?: string): Promise<void>;
|
|
87
104
|
/**
|
|
88
105
|
* For a site with an admin address (its config's `admin`, which the exported
|
|
89
106
|
* `goodfellow-site.json` carries), puts the guard first in every page of the
|
|
@@ -143,12 +160,18 @@ export declare function exportedPages(outDir: string): Promise<BuiltPage[]>;
|
|
|
143
160
|
*/
|
|
144
161
|
export declare function checkPages(pages: readonly BuiltPage[], root?: string): Promise<SiteProblem[]>;
|
|
145
162
|
/**
|
|
146
|
-
* Runs one of a plugin's tasks for the admin panel, as
|
|
147
|
-
* does for other sites, with the site's config loaded in
|
|
148
|
-
* answer sealed to the request's key, even when the task
|
|
149
|
-
*
|
|
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`.
|
|
150
171
|
*/
|
|
151
172
|
export declare function writeTaskResult(root: string, request: PluginTaskRequest, out?: string): Promise<TaskResultFile>;
|
|
173
|
+
/** The site's config, loaded in Node, for `writeTaskResult()`'s process that loads it apart from the job's secrets. */
|
|
174
|
+
export declare function loadTaskConfig(root: string): Promise<GoodfellowConfig>;
|
|
152
175
|
/**
|
|
153
176
|
* Renders a page with a block from its own file as well as the site's config's,
|
|
154
177
|
* for `goodfellow-next blocks add`'s check that a block it made renders: both
|
package/dist/export.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { a as loadSiteModule, i as loadSiteConfig, n as buildPluginFrames, r as configHasPlugins } from "./plugins-
|
|
2
|
-
import {
|
|
1
|
+
import { a as loadSiteModule, i as loadSiteConfig, n as buildPluginFrames, r as configHasPlugins } from "./plugins-UKkfu0Uj.js";
|
|
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";
|
|
3
4
|
import { CALENDARS_DIR, CLOUDFLARE_REDIRECTS_PATH, EDITOR_PRELUDE, FEEDS_DIR, SITE_DESCRIPTION_PATH, TASK_RESULT_FILE, absoluteUrl, allPages, calendarFiles, cloudflareRedirects, editorPolicy, feedFiles, hasLanguages, loadSiteContent, parseSiteDescription, pathLanguage, pluginId, redirectFiles, rootRobotsUrl, sealTaskAnswer, shownContent, siteLanguages, sitePages, taskRequestProblem, todayIn } from "@goodfellow-cms/core";
|
|
4
|
-
import { BUILD_DATES_DIR, builtSiteSource, guardBuiltSite, reportedDate, runPluginBuildSteps, runPluginTask, selfHostFonts, taskSecretsFrom, writeBuildRecord } from "@goodfellow-cms/core/node";
|
|
5
|
+
import { BUILD_DATES_DIR, builtSiteSource, guardBuiltSite, reportedDate, runPluginBuildSteps, runPluginTask, selfHostFonts, sitePluginsApart, taskSecretsFrom, writeBuildRecord, writeImageSizes, writeSiteHeaders } from "@goodfellow-cms/core/node";
|
|
5
6
|
import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
6
7
|
import { dirname, join, relative, resolve } from "node:path";
|
|
7
8
|
import { createHash } from "node:crypto";
|
|
@@ -70,16 +71,34 @@ async function buildFileFolders(root = process.cwd()) {
|
|
|
70
71
|
const { settings } = await loadSiteContent(builtSiteSource(resolve(root)));
|
|
71
72
|
return siteLanguages(settings).flatMap((language) => [CALENDARS_DIR, FEEDS_DIR].map((dir) => `${language.prefix.slice(1)}${language.prefix ? "/" : ""}${dir}`));
|
|
72
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
|
+
}
|
|
73
79
|
/** The `basePath` the export was built with, which Next.js records in `.next/routes-manifest.json`. */
|
|
74
80
|
async function exportBasePath(root) {
|
|
75
81
|
try {
|
|
76
|
-
const manifest = JSON.parse(await readFile(join(root, "
|
|
82
|
+
const manifest = JSON.parse(await readFile(join(await nextDistDir(root), "routes-manifest.json"), "utf8"));
|
|
77
83
|
return typeof manifest.basePath === "string" ? manifest.basePath : "";
|
|
78
84
|
} catch {
|
|
79
85
|
return "";
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
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
|
+
/**
|
|
83
102
|
* Plugins' parts that builds make: each plugin's admin frame, beside the
|
|
84
103
|
* editor's frames at `admin/plugins/<id>/`, and its build steps, each in a
|
|
85
104
|
* process of its own held to what the plugin declared. Into a static export
|
|
@@ -137,7 +156,7 @@ async function writeFontFiles(outDir, root = process.cwd()) {
|
|
|
137
156
|
async function writeBuildRecordFile(outDir, root = process.cwd()) {
|
|
138
157
|
const dir = resolve(root);
|
|
139
158
|
const content = await loadSiteContent(builtSiteSource(dir));
|
|
140
|
-
const date = await reportedDate(join(dir,
|
|
159
|
+
const date = await reportedDate(join(await nextDistDir(dir), BUILD_DATES_DIR));
|
|
141
160
|
return (await writeBuildRecord(outDir, {
|
|
142
161
|
root: dir,
|
|
143
162
|
content,
|
|
@@ -168,7 +187,7 @@ async function writeRedirectFiles(outDir, root = process.cwd()) {
|
|
|
168
187
|
await writeFile(target, file.content);
|
|
169
188
|
written.push(file);
|
|
170
189
|
}
|
|
171
|
-
const cloudflare = cloudflareRedirects(written, base);
|
|
190
|
+
const cloudflare = cloudflareRedirects(written, base, content.settings.trailingSlash !== false);
|
|
172
191
|
const redirects = join(outDir, CLOUDFLARE_REDIRECTS_PATH.slice(1));
|
|
173
192
|
if (cloudflare.file && !await exists(redirects)) await writeFile(redirects, cloudflare.file.content);
|
|
174
193
|
return {
|
|
@@ -177,6 +196,41 @@ async function writeRedirectFiles(outDir, root = process.cwd()) {
|
|
|
177
196
|
};
|
|
178
197
|
}
|
|
179
198
|
/**
|
|
199
|
+
* Writes the smaller copies of the media library's photos that the export's
|
|
200
|
+
* pages list in their `srcset` (`writeImageSizes()`), with sharp, where
|
|
201
|
+
* `next build` said they list them (`IMAGE_SIZES_MARK`): a static export
|
|
202
|
+
* whose images Next.js doesn't optimize. `undefined` where they don't.
|
|
203
|
+
*/
|
|
204
|
+
async function writeImageFiles(outDir, root = process.cwd()) {
|
|
205
|
+
const dir = resolve(root);
|
|
206
|
+
if (!await exists(join(await nextDistDir(dir), "goodfellow-image-sizes"))) return void 0;
|
|
207
|
+
return writeImageSizes(dir, outDir, { resize: await sharpResizer() });
|
|
208
|
+
}
|
|
209
|
+
/** Makes the copies with sharp, as WebP, turned as each photo's details say and without them, unless it can't load here. */
|
|
210
|
+
async function sharpResizer() {
|
|
211
|
+
try {
|
|
212
|
+
const { default: sharp } = await import("sharp");
|
|
213
|
+
return async (image, width) => new Uint8Array(await sharp(image).rotate().resize({ width }).webp({ quality: 80 }).toBuffer());
|
|
214
|
+
} catch {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Adds Cloudflare Pages' header rules for every site to the export's
|
|
220
|
+
* `_headers`, as `goodfellow build` does (`writeSiteHeaders()`), from the
|
|
221
|
+
* admin address and broker the exported `goodfellow-site.json` names.
|
|
222
|
+
*/
|
|
223
|
+
async function writeExportHeaders(outDir, root = process.cwd()) {
|
|
224
|
+
const text = await readFile(join(outDir, SITE_DESCRIPTION_PATH), "utf8").catch(() => void 0);
|
|
225
|
+
const parsed = text === void 0 ? void 0 : parseSiteDescription(text);
|
|
226
|
+
const description = parsed?.ok ? parsed.description : void 0;
|
|
227
|
+
await writeSiteHeaders(outDir, {
|
|
228
|
+
base: `${await exportBasePath(resolve(root))}/`,
|
|
229
|
+
...description?.admin && { admin: description.admin },
|
|
230
|
+
...description?.broker && { broker: description.broker }
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
180
234
|
* For a site with an admin address (its config's `admin`, which the exported
|
|
181
235
|
* `goodfellow-site.json` carries), puts the guard first in every page of the
|
|
182
236
|
* export and adds Cloudflare Pages' header rules, as `goodfellow build` does.
|
|
@@ -275,7 +329,7 @@ async function writePageLanguages(outDir, root = process.cwd()) {
|
|
|
275
329
|
const dir = resolve(root);
|
|
276
330
|
const { settings } = await loadSiteContent(builtSiteSource(dir));
|
|
277
331
|
if (!hasLanguages(settings)) return 0;
|
|
278
|
-
const base = outDir ?? join(dir, "
|
|
332
|
+
const base = outDir ?? join(await nextDistDir(dir), "server/app");
|
|
279
333
|
const files = (await readdir(base, { recursive: true })).map((file) => file.split("\\").join("/")).filter((file) => file.endsWith(".html") && !/^(admin|_next|pagefind)\//.test(file));
|
|
280
334
|
let changed = 0;
|
|
281
335
|
for (const file of files) {
|
|
@@ -295,7 +349,7 @@ async function writePageLanguages(outDir, root = process.cwd()) {
|
|
|
295
349
|
* and keeps their HTML in `.next/server/app/`. The admin panel and Next.js's own pages are left out.
|
|
296
350
|
*/
|
|
297
351
|
async function builtPages(root = process.cwd()) {
|
|
298
|
-
const next =
|
|
352
|
+
const next = await nextDistDir(resolve(root));
|
|
299
353
|
const manifest = JSON.parse(await readFile(join(next, "prerender-manifest.json"), "utf8"));
|
|
300
354
|
const trailingSlash = JSON.parse(await readFile(join(next, "required-server-files.json"), "utf8")).config?.trailingSlash === true;
|
|
301
355
|
const pages = [];
|
|
@@ -353,10 +407,14 @@ async function checkPages(pages, root = process.cwd()) {
|
|
|
353
407
|
}));
|
|
354
408
|
}
|
|
355
409
|
/**
|
|
356
|
-
* Runs one of a plugin's tasks for the admin panel, as
|
|
357
|
-
* does for other sites, with the site's config loaded in
|
|
358
|
-
* answer sealed to the request's key, even when the task
|
|
359
|
-
*
|
|
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`.
|
|
360
418
|
*/
|
|
361
419
|
async function writeTaskResult(root, request, out = TASK_RESULT_FILE) {
|
|
362
420
|
const secrets = taskSecretsFrom(process.env);
|
|
@@ -364,7 +422,10 @@ async function writeTaskResult(root, request, out = TASK_RESULT_FILE) {
|
|
|
364
422
|
if (problem) throw new Error(problem);
|
|
365
423
|
let answer;
|
|
366
424
|
try {
|
|
367
|
-
const plugin = (
|
|
425
|
+
const plugin = (await sitePluginsApart(resolve(root), {
|
|
426
|
+
module: import.meta.url,
|
|
427
|
+
export: "loadTaskConfig"
|
|
428
|
+
})).find((candidate) => pluginId(candidate.name) === request.plugin);
|
|
368
429
|
answer = plugin ? await runPluginTask({
|
|
369
430
|
root: resolve(root),
|
|
370
431
|
plugin,
|
|
@@ -385,6 +446,10 @@ async function writeTaskResult(root, request, out = TASK_RESULT_FILE) {
|
|
|
385
446
|
await writeFile(resolve(root, out), `${JSON.stringify(sealed)}\n`);
|
|
386
447
|
return sealed;
|
|
387
448
|
}
|
|
449
|
+
/** The site's config, loaded in Node, for `writeTaskResult()`'s process that loads it apart from the job's secrets. */
|
|
450
|
+
function loadTaskConfig(root) {
|
|
451
|
+
return loadSiteConfig(root);
|
|
452
|
+
}
|
|
388
453
|
/**
|
|
389
454
|
* Renders a page with a block from its own file as well as the site's config's,
|
|
390
455
|
* for `goodfellow-next blocks add`'s check that a block it made renders: both
|
|
@@ -410,4 +475,4 @@ function blockPageRender(root) {
|
|
|
410
475
|
};
|
|
411
476
|
}
|
|
412
477
|
//#endregion
|
|
413
|
-
export { EDITOR_PAGES, NEXT_SITE_FILES, blockPageRender, buildFileFolders, builtPages, checkPages, exportedPages, fixSegmentFiles, guardExport, writeBuildRecordFile, writeCalendarFiles, writeContentApi, writeEditorPolicy, writeFeedFiles, writeFontFiles, writePageLanguages, writePluginFiles, writeRedirectFiles, writeTaskResult };
|
|
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 };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/image-sizes-mark.ts
|
|
2
|
+
/**
|
|
3
|
+
* Set while `next build` renders a static export whose images Next.js doesn't
|
|
4
|
+
* optimize, so pages list the smaller copies of the media library's photos
|
|
5
|
+
* (`imageWidths`) that `goodfellow-next finish` writes.
|
|
6
|
+
*/
|
|
7
|
+
const IMAGE_SIZES_ENV = "GOODFELLOW_IMAGE_SIZES";
|
|
8
|
+
/** The file in `.next/` that tells `goodfellow-next finish` the pages list them. */
|
|
9
|
+
const IMAGE_SIZES_MARK = "goodfellow-image-sizes";
|
|
10
|
+
/**
|
|
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";
|
|
16
|
+
//#endregion
|
|
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,7 +1,8 @@
|
|
|
1
|
-
import { ByLanguage, CurrentMenuLinks, NextImage, NextLink, PageLanguage } from "./components.js";
|
|
2
|
-
import
|
|
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";
|
|
3
4
|
import { BUILD_DATES_ENV, builtSiteSource, fileSystemSource, fontStandIns, readMediaSizes, readStylesheet, reportDatesTo } from "@goodfellow-cms/core/node";
|
|
4
|
-
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";
|
|
5
6
|
import { notFound, redirect } from "next/navigation";
|
|
6
7
|
import { cache } from "react";
|
|
7
8
|
import { preload } from "react-dom";
|
|
@@ -19,12 +20,31 @@ function configuredBasePath() {
|
|
|
19
20
|
return process.env.GOODFELLOW_BASE_PATH ?? "";
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
23
|
+
* Where a full-stack site's build copied its Google Fonts (`hostFonts()`), for
|
|
24
|
+
* the stylesheet at `source`. `withGoodfellow()` puts them in the env, which
|
|
25
|
+
* Next.js writes into this code by its name, so pages it renders later have
|
|
26
|
+
* them too. Static exports have none: `goodfellow-next finish` copies theirs.
|
|
27
|
+
*/
|
|
28
|
+
function hostedFonts(source) {
|
|
29
|
+
const value = process.env.GOODFELLOW_FONTS;
|
|
30
|
+
if (!value) return void 0;
|
|
31
|
+
try {
|
|
32
|
+
const fonts = JSON.parse(value);
|
|
33
|
+
return fonts.source === source ? fonts : void 0;
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
22
39
|
* Reads the site's content from disk, as visitors see it now: without pages
|
|
23
40
|
* and items scheduled for later, or hidden again (`shownContent()`). Cached
|
|
24
41
|
* for the length of one request, so a page and its metadata read it once, and
|
|
25
42
|
* content edits show on reload in development.
|
|
26
43
|
*/
|
|
27
|
-
const loadSite = cache(async (root = process.cwd()) => shownContent(await loadSiteContent(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
|
+
}))));
|
|
28
48
|
function pathOf(segments) {
|
|
29
49
|
try {
|
|
30
50
|
return normalizePagePath(`/${(segments ?? []).map((segment) => decodeURIComponent(segment)).join("/")}`);
|
|
@@ -39,20 +59,49 @@ function exportedPages(content, blocks) {
|
|
|
39
59
|
function findPage(content, blocks, path) {
|
|
40
60
|
return exportedPages(content, blocks).find((page) => page.path === path);
|
|
41
61
|
}
|
|
62
|
+
/** Whether the site asks visitors before loading other sites' content. */
|
|
63
|
+
function asks(content) {
|
|
64
|
+
return content.settings.consent !== void 0;
|
|
65
|
+
}
|
|
66
|
+
/** The consent script, after the footer's notice, on a site that asks. */
|
|
67
|
+
function ConsentAsk({ content }) {
|
|
68
|
+
if (!asks(content)) return null;
|
|
69
|
+
return /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: consentScript() } });
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* What the built-in blocks need in the browser (copying code, the Dark mode
|
|
73
|
+
* switch, search), each a small script that works on what navigations add.
|
|
74
|
+
* `goodfellow build` adds each only to pages that use it; a layout can't know
|
|
75
|
+
* the page, so it has them all, but the Dark mode switch's only where the
|
|
76
|
+
* site has dark mode, since it shows nothing otherwise.
|
|
77
|
+
*/
|
|
78
|
+
function BlockScripts({ content }) {
|
|
79
|
+
const scripts = BLOCK_SCRIPTS.filter(({ attribute }) => attribute !== DARK_MODE_ATTRIBUTE || content.settings.theme.darkMode);
|
|
80
|
+
return /* @__PURE__ */ jsx(Fragment, { children: scripts.map(({ attribute, script }) => /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: script() } }, attribute)) });
|
|
81
|
+
}
|
|
42
82
|
function toAbsolute(siteUrl, url) {
|
|
43
83
|
if (!url || !siteUrl || !url.startsWith("/") || url.startsWith("//")) return url;
|
|
44
84
|
return absoluteUrl(siteUrl, url);
|
|
45
85
|
}
|
|
46
|
-
/**
|
|
47
|
-
|
|
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 = {}) {
|
|
48
91
|
const head = pageHead(content, page);
|
|
49
92
|
const socialImage = toAbsolute(content.settings.url, head.socialImage);
|
|
93
|
+
const socialSize = head.socialImage ? media[head.socialImage] : void 0;
|
|
50
94
|
const feeds = feedLinks(contentForPage(content, page));
|
|
51
95
|
return {
|
|
52
96
|
...content.settings.url && { metadataBase: new URL(content.settings.url) },
|
|
53
97
|
title: { absolute: head.title },
|
|
54
98
|
...head.description && { description: head.description },
|
|
55
|
-
|
|
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
|
+
},
|
|
56
105
|
...(head.canonicalUrl || feeds.length > 0) && { alternates: {
|
|
57
106
|
...head.canonicalUrl && { canonical: head.canonicalUrl },
|
|
58
107
|
...head.alternates && { languages: Object.fromEntries(head.alternates.map(({ language, href }) => [language, href])) },
|
|
@@ -63,12 +112,25 @@ function pageMetadata(content, page, basePath = configuredBasePath()) {
|
|
|
63
112
|
} },
|
|
64
113
|
...head.favicon && { icons: { icon: withBase(head.favicon, basePath) } },
|
|
65
114
|
openGraph: {
|
|
66
|
-
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,
|
|
67
121
|
title: head.title,
|
|
68
122
|
...head.description && { description: head.description },
|
|
69
123
|
...head.canonicalUrl && { url: head.canonicalUrl },
|
|
70
|
-
...socialImage && /^https?:\/\//.test(socialImage) && { images: [
|
|
71
|
-
|
|
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 }
|
|
72
134
|
};
|
|
73
135
|
}
|
|
74
136
|
/**
|
|
@@ -90,17 +152,29 @@ function goodfellowPages(config, options = {}) {
|
|
|
90
152
|
media: {
|
|
91
153
|
...await readMediaSizes(root),
|
|
92
154
|
...storedMediaSizes((await site()).mediaStorage)
|
|
93
|
-
}
|
|
155
|
+
},
|
|
156
|
+
...process.env["GOODFELLOW_IMAGE_SIZES"] && { imageWidths: IMAGE_WIDTHS }
|
|
94
157
|
});
|
|
95
158
|
/** The site's theme and fonts, which React moves into the document's head. themeToCss escapes its output. */
|
|
96
159
|
async function Theme({ content }) {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
160
|
+
const googleUrl = googleFontsUrl(content.settings.theme);
|
|
161
|
+
const hosted = googleUrl ? hostedFonts(googleUrl) : void 0;
|
|
162
|
+
const fontsUrl = googleUrl && hosted ? withSelfHostedFonts(googleUrl, hosted, basePath()) : googleUrl;
|
|
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()), {
|
|
100
165
|
as: "font",
|
|
101
166
|
type: "font/woff2",
|
|
102
167
|
crossOrigin: ""
|
|
103
168
|
});
|
|
169
|
+
for (const file of fontFiles) {
|
|
170
|
+
const href = hosted ? withSelfHostedFonts(file, hosted, basePath()) : file;
|
|
171
|
+
if (hosted && href === file) continue;
|
|
172
|
+
preload(href, {
|
|
173
|
+
as: "font",
|
|
174
|
+
type: "font/woff2",
|
|
175
|
+
crossOrigin: ""
|
|
176
|
+
});
|
|
177
|
+
}
|
|
104
178
|
const colorScheme = colorSchemeMeta(content.settings.theme);
|
|
105
179
|
const schemeScript = colorSchemeScript(content.settings.theme);
|
|
106
180
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -112,7 +186,7 @@ function goodfellowPages(config, options = {}) {
|
|
|
112
186
|
/* @__PURE__ */ jsx("style", {
|
|
113
187
|
href: "goodfellow-theme",
|
|
114
188
|
precedence: "default",
|
|
115
|
-
children: themeToCss(content.settings.theme, standIns)
|
|
189
|
+
children: themeToCss(content.settings.theme, standIns, { base: basePath() })
|
|
116
190
|
}),
|
|
117
191
|
fontsUrl && /* @__PURE__ */ jsx("link", {
|
|
118
192
|
rel: "stylesheet",
|
|
@@ -129,7 +203,8 @@ function goodfellowPages(config, options = {}) {
|
|
|
129
203
|
async generateMetadata({ params }) {
|
|
130
204
|
const content = await site();
|
|
131
205
|
const page = findPage(content, config.blocks, pathOf((await params).path));
|
|
132
|
-
|
|
206
|
+
if (!page) return {};
|
|
207
|
+
return pageMetadata(content, applyPageEntry(configs, content, page), basePath(), (await renderOptions()).media);
|
|
133
208
|
},
|
|
134
209
|
/**
|
|
135
210
|
* For `app/admin/page.tsx`: what the admin panel's page knows about the
|
|
@@ -197,12 +272,27 @@ function goodfellowPages(config, options = {}) {
|
|
|
197
272
|
}
|
|
198
273
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
199
274
|
/* @__PURE__ */ jsx(Theme, { content }),
|
|
200
|
-
/* @__PURE__ */ jsx(HeadCode, {
|
|
275
|
+
/* @__PURE__ */ jsx(HeadCode, {
|
|
276
|
+
code: content.code?.head,
|
|
277
|
+
held: heldCode(content.settings.consent, "head"),
|
|
278
|
+
siteUrl: content.settings.url
|
|
279
|
+
}),
|
|
280
|
+
/* @__PURE__ */ jsx(StatsCode, {
|
|
281
|
+
stats: content.settings.stats,
|
|
282
|
+
held: heldStats(content.settings.consent, content.settings.stats)
|
|
283
|
+
}),
|
|
201
284
|
header,
|
|
202
285
|
children,
|
|
203
286
|
footer,
|
|
204
287
|
/* @__PURE__ */ jsx(CurrentMenuLinks, { basePath: basePath() }),
|
|
205
|
-
/* @__PURE__ */ jsx(
|
|
288
|
+
content.settings.stats?.service === "goatcounter" && process.env.NODE_ENV !== "development" && /* @__PURE__ */ jsx(GoatCounterNavigation, {}),
|
|
289
|
+
/* @__PURE__ */ jsx(SearchResultLinks, { basePath: basePath() }),
|
|
290
|
+
/* @__PURE__ */ jsx(BodyCode, {
|
|
291
|
+
code: content.code?.body,
|
|
292
|
+
held: heldCode(content.settings.consent, "body")
|
|
293
|
+
}),
|
|
294
|
+
/* @__PURE__ */ jsx(ConsentAsk, { content }),
|
|
295
|
+
/* @__PURE__ */ jsx(BlockScripts, { content })
|
|
206
296
|
] });
|
|
207
297
|
},
|
|
208
298
|
/** The page at the route's address, inside `Layout`'s header and footer. */
|
|
@@ -237,7 +327,11 @@ function goodfellowPages(config, options = {}) {
|
|
|
237
327
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
238
328
|
/* @__PURE__ */ jsx("title", { children: pageHead(content, page).title }),
|
|
239
329
|
/* @__PURE__ */ jsx(Theme, { content }),
|
|
240
|
-
/* @__PURE__ */ jsx(HeadCode, {
|
|
330
|
+
/* @__PURE__ */ jsx(HeadCode, {
|
|
331
|
+
code: content.code?.head,
|
|
332
|
+
held: heldCode(content.settings.consent, "head"),
|
|
333
|
+
siteUrl: content.settings.url
|
|
334
|
+
}),
|
|
241
335
|
/* @__PURE__ */ jsx(PageBody, {
|
|
242
336
|
site: prepared.site,
|
|
243
337
|
pageConfig: prepared.pageConfig,
|
|
@@ -246,7 +340,13 @@ function goodfellowPages(config, options = {}) {
|
|
|
246
340
|
header: prepared.header,
|
|
247
341
|
footer: prepared.footer
|
|
248
342
|
}),
|
|
249
|
-
/* @__PURE__ */ jsx(BodyCode, {
|
|
343
|
+
/* @__PURE__ */ jsx(BodyCode, {
|
|
344
|
+
code: content.code?.body,
|
|
345
|
+
held: heldCode(content.settings.consent, "body")
|
|
346
|
+
}),
|
|
347
|
+
/* @__PURE__ */ jsx(ConsentAsk, { content }),
|
|
348
|
+
/* @__PURE__ */ jsx(BlockScripts, { content }),
|
|
349
|
+
/* @__PURE__ */ jsx(SearchResultLinks, { basePath: basePath() })
|
|
250
350
|
] });
|
|
251
351
|
},
|
|
252
352
|
/** For `app/sitemap.ts`: every page but "Page not found" and pages hidden from search engines, once the site's address is set. */
|
|
@@ -273,15 +373,15 @@ function goodfellowPages(config, options = {}) {
|
|
|
273
373
|
siteDescription() {
|
|
274
374
|
return Response.json(describedForCopy(describeSite(config), siteCopyFrom(process.env)));
|
|
275
375
|
},
|
|
276
|
-
/**
|
|
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
|
+
*/
|
|
277
380
|
async robots() {
|
|
278
|
-
const
|
|
381
|
+
const { settings } = await site();
|
|
279
382
|
return {
|
|
280
|
-
rules:
|
|
281
|
-
|
|
282
|
-
allow: "/"
|
|
283
|
-
},
|
|
284
|
-
...url && { sitemap: absoluteUrl(url, "/sitemap.xml") }
|
|
383
|
+
rules: robotsRules(settings),
|
|
384
|
+
...settings.url && { sitemap: absoluteUrl(settings.url, "/sitemap.xml") }
|
|
285
385
|
};
|
|
286
386
|
}
|
|
287
387
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EDITOR_PRELUDE, INSTALLED_PLUGINS_INDEX, PLUGINS_PATH, framedPlugins, pluginFrameEntry, pluginFramePage, pluginId } from "@goodfellow-cms/core";
|
|
2
|
-
import { LICENSES_FILE, licensesPlugin } from "@goodfellow-cms/core/node";
|
|
2
|
+
import { LICENSES_FILE, licensesPlugin, mediaStorageOrigins } from "@goodfellow-cms/core/node";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { isAbsolute, join, resolve } from "node:path";
|
|
@@ -125,7 +125,8 @@ async function buildPluginFrame(plugin, { root, dir, watch }) {
|
|
|
125
125
|
scripts: ["plugin.js"],
|
|
126
126
|
stylesheets: ["plugin.css"],
|
|
127
127
|
licenses: LICENSES_FILE,
|
|
128
|
-
editor: Boolean(plugin.editor)
|
|
128
|
+
editor: Boolean(plugin.editor),
|
|
129
|
+
...plugin.editor && { media: await mediaStorageOrigins(root) }
|
|
129
130
|
}));
|
|
130
131
|
};
|
|
131
132
|
const options = {
|
package/dist/server.d.ts
CHANGED
|
@@ -38,6 +38,13 @@ export interface GoodfellowApiOptions {
|
|
|
38
38
|
* `plugins/<id>/…`, and runs with everything the server has.
|
|
39
39
|
*/
|
|
40
40
|
plugins?: AppApiOptions["plugins"];
|
|
41
|
+
/**
|
|
42
|
+
* Whether owners' changes to the site's code, such as adding blocks on the
|
|
43
|
+
* Blocks screen, go to a developer for review (`"review"`, the default),
|
|
44
|
+
* since this server runs that code with its token and keys, or are published
|
|
45
|
+
* (`"publish"`).
|
|
46
|
+
*/
|
|
47
|
+
codeChanges?: AppApiOptions["codeChanges"];
|
|
41
48
|
}
|
|
42
49
|
/** The route parameters of a catch-all route named `[...path]`. */
|
|
43
50
|
interface RouteContext {
|
package/dist/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ownersOnlyFields } from "@goodfellow-cms/core";
|
|
1
2
|
import { appApi } from "@goodfellow-cms/core/node";
|
|
2
3
|
import { aiServer } from "@goodfellow-cms/ai";
|
|
3
4
|
//#region src/server.ts
|
|
@@ -32,7 +33,9 @@ function goodfellowApi(options) {
|
|
|
32
33
|
...options.signOut && { signOut: options.signOut },
|
|
33
34
|
...ai && ai.services.length > 0 && { ai },
|
|
34
35
|
...options.onError && { onError: options.onError },
|
|
35
|
-
...options.plugins && { plugins: options.plugins }
|
|
36
|
+
...options.plugins && { plugins: options.plugins },
|
|
37
|
+
...options.codeChanges && { codeChanges: options.codeChanges },
|
|
38
|
+
ownersOnly: ownersOnlyFields(options.config.blocks ?? {})
|
|
36
39
|
});
|
|
37
40
|
return api;
|
|
38
41
|
}
|