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