@goodfellow-cms/next 0.1.0 → 0.3.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/README.md +2 -2
- package/bin/goodfellow-next.js +26 -4
- package/dist/admin.js +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +37 -4
- package/dist/export.d.ts +12 -0
- package/dist/export.js +54 -3
- package/dist/index.d.ts +7 -0
- package/dist/index.js +73 -31
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@ Goodfellow pages and the admin panel in a Next.js App Router site, exported as s
|
|
|
4
4
|
|
|
5
5
|
Start from the Next.js starter: `npm create goodfellow@latest my-site -- --template next`.
|
|
6
6
|
|
|
7
|
-
[Goodfellow](https://github.com/
|
|
7
|
+
[Goodfellow](https://github.com/goodfellow-cms/goodfellow-cms) is a website builder for people who aren't developers, with no server or database: pages are files in your own GitHub or GitLab repository, edited in a visual editor built on [Puck](https://puckeditor.com). To start a new site, run `npm create goodfellow@latest my-site`. See the [documentation](https://goodfellow-cms.github.io/goodfellow-cms/) for everything else.
|
|
8
8
|
|
|
9
|
-
More in the docs: [Next.js](https://
|
|
9
|
+
More in the docs: [Next.js](https://goodfellow-cms.github.io/goodfellow-cms/docs/nextjs).
|
|
10
10
|
|
|
11
11
|
## License
|
|
12
12
|
|
package/bin/goodfellow-next.js
CHANGED
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// After `next build`: `goodfellow-next finish out` fixes the export's prefetch files on Windows
|
|
3
|
-
// and
|
|
2
|
+
// After `next build`: `goodfellow-next finish out` fixes the export's prefetch files on Windows,
|
|
3
|
+
// writes the calendar files of collections of events and the pages at redirects' old addresses, and the search index for sites with a search block. `index` only writes the index.
|
|
4
|
+
// `goodfellow-next update` updates Goodfellow, as `goodfellow update` does for other sites.
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { formatUpdateResult } from "@goodfellow-cms/core";
|
|
7
|
+
import { update } from "@goodfellow-cms/core/node";
|
|
4
8
|
import { writeSearchIndex } from "@goodfellow-cms/core/search-index";
|
|
5
|
-
import { fixSegmentFiles } from "@goodfellow-cms/next/export";
|
|
9
|
+
import { fixSegmentFiles, writeCalendarFiles, writeRedirectFiles } from "@goodfellow-cms/next/export";
|
|
6
10
|
|
|
7
11
|
const [command, dir = "out"] = process.argv.slice(2);
|
|
12
|
+
if (command === "update") {
|
|
13
|
+
const { values } = parseArgs({
|
|
14
|
+
args: process.argv.slice(3),
|
|
15
|
+
options: { to: { type: "string" }, publish: { type: "boolean" } },
|
|
16
|
+
});
|
|
17
|
+
const result = await update({ to: values.to, publish: values.publish });
|
|
18
|
+
// For the admin panel, which reads it in the host's log.
|
|
19
|
+
console.log(formatUpdateResult(result));
|
|
20
|
+
process.exit(result.state === "failed" ? 1 : 0);
|
|
21
|
+
}
|
|
8
22
|
if (command !== "finish" && command !== "index") {
|
|
9
23
|
console.error(
|
|
10
24
|
[
|
|
11
25
|
"Usage: goodfellow-next finish [folder]",
|
|
12
26
|
" goodfellow-next index [folder]",
|
|
27
|
+
" goodfellow-next update [--to fixes|automatic|latest|<release>] [--publish]",
|
|
13
28
|
"",
|
|
14
29
|
"finish Gets the exported site in folder (default: out) ready to publish: puts the files",
|
|
15
|
-
" Next.js uses to load pages ahead where browsers look for them,
|
|
30
|
+
" Next.js uses to load pages ahead where browsers look for them, writes calendar files",
|
|
31
|
+
" for collections of events and pages that send visitors on from old addresses, and",
|
|
32
|
+
" indexes it for search.",
|
|
16
33
|
"index Only indexes the exported site for its search block.",
|
|
34
|
+
"update Updates Goodfellow, and blocks from its registry, if the site still builds.",
|
|
17
35
|
].join("\n"),
|
|
18
36
|
);
|
|
19
37
|
process.exit(1);
|
|
@@ -21,6 +39,10 @@ if (command !== "finish" && command !== "index") {
|
|
|
21
39
|
if (command === "finish") {
|
|
22
40
|
const moved = await fixSegmentFiles(dir);
|
|
23
41
|
if (moved > 0) console.log(`Moved ${moved} of Next.js's prefetch files to where browsers look for them`);
|
|
42
|
+
const calendars = await writeCalendarFiles(dir);
|
|
43
|
+
if (calendars > 0) console.log(`Wrote ${calendars} calendar files`);
|
|
44
|
+
const redirects = await writeRedirectFiles(dir);
|
|
45
|
+
if (redirects > 0) console.log(`Wrote ${redirects} redirects`);
|
|
24
46
|
}
|
|
25
47
|
const indexed = await writeSearchIndex(dir);
|
|
26
48
|
console.log(
|
package/dist/admin.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Admin } from "@goodfellow-cms/admin";
|
|
|
4
4
|
import "@puckeditor/core/puck.css";
|
|
5
5
|
import "@goodfellow-cms/admin/styles.css";
|
|
6
6
|
//#region src/generated/theme-css.ts
|
|
7
|
-
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@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 h1,\n h2,\n h3,\n h4,\n h5,\n h6 {\n font-family: var(--font-heading);\n }\n}\n";
|
|
7
|
+
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";
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region src/admin.tsx
|
|
10
10
|
/**
|
package/dist/config.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface GoodfellowNextOptions {
|
|
|
14
14
|
* - Serves the site from `basePath`, or else the `GOODFELLOW_BASE` environment
|
|
15
15
|
* variable, as `goodfellow build` does (GitHub Pages serves most sites from `/repository-name/`).
|
|
16
16
|
* - Serves images as they are unless the site sets an image loader, since static files can't be optimized on request.
|
|
17
|
+
* - Adds the site's redirects (Site Settings → Redirects) ahead of the site's own, for hosts that
|
|
18
|
+
* send them, such as Vercel. Others get the pages `goodfellow-next finish` writes at old addresses.
|
|
17
19
|
* - In `next dev`, runs the admin panel's local backend, so publishing saves to
|
|
18
20
|
* the files on disk. It's never part of a build.
|
|
19
21
|
* - In `next dev`, turns off React's debug channel (`experimental.reactDebugChannel`)
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { normalizeBase } from "@goodfellow-cms/core";
|
|
2
|
-
import { DEV_API_PREFIX, handleDevApi, isLocalOrigin, localFileStore, localRegistryDir } from "@goodfellow-cms/core/node";
|
|
1
|
+
import { CALENDARS_DIR, allPages, loadSiteContent, normalizeBase, redirectFrom, redirectProblems } from "@goodfellow-cms/core";
|
|
2
|
+
import { DEV_API_PREFIX, fileSystemSource, handleDevApi, isLocalOrigin, localFileStore, localRegistryDir } from "@goodfellow-cms/core/node";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
//#region src/config.ts
|
|
@@ -53,6 +53,32 @@ function imageSettings(images) {
|
|
|
53
53
|
...images
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
+
/** An address as Next.js matches a redirect's source, with the characters its patterns use escaped. */
|
|
57
|
+
function redirectSource(path) {
|
|
58
|
+
return encodeURI(path).replace(/[()[\]{}+*?:!]/g, "\\$&");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The site's redirects (`content/redirects.json`) for hosts that send them
|
|
62
|
+
* themselves, such as Vercel, which reads them from the build. Redirects from
|
|
63
|
+
* an address a page or a collection's item has are left out, since pages win,
|
|
64
|
+
* as they do over the pages `goodfellow-next finish` writes at old addresses.
|
|
65
|
+
* Content with a problem gives none, and the build reports the problem.
|
|
66
|
+
*/
|
|
67
|
+
async function siteRedirects(root) {
|
|
68
|
+
const content = await loadSiteContent(fileSystemSource(root)).catch(() => void 0);
|
|
69
|
+
if (!content?.redirects) return [];
|
|
70
|
+
const taken = new Set(allPages(content).map((page) => page.path));
|
|
71
|
+
const problems = redirectProblems(content.redirects);
|
|
72
|
+
return content.redirects.flatMap((redirect, index) => {
|
|
73
|
+
const from = redirectFrom(redirect.from);
|
|
74
|
+
if (problems[index] || !from.ok || taken.has(from.path)) return [];
|
|
75
|
+
return [{
|
|
76
|
+
source: redirectSource(from.path),
|
|
77
|
+
destination: redirect.to.trim(),
|
|
78
|
+
permanent: true
|
|
79
|
+
}];
|
|
80
|
+
});
|
|
81
|
+
}
|
|
56
82
|
/**
|
|
57
83
|
* Sets up Next.js for a Goodfellow site, in `next.config.ts`:
|
|
58
84
|
* `export default withGoodfellow({ ... })`.
|
|
@@ -63,6 +89,8 @@ function imageSettings(images) {
|
|
|
63
89
|
* - Serves the site from `basePath`, or else the `GOODFELLOW_BASE` environment
|
|
64
90
|
* variable, as `goodfellow build` does (GitHub Pages serves most sites from `/repository-name/`).
|
|
65
91
|
* - Serves images as they are unless the site sets an image loader, since static files can't be optimized on request.
|
|
92
|
+
* - Adds the site's redirects (Site Settings → Redirects) ahead of the site's own, for hosts that
|
|
93
|
+
* send them, such as Vercel. Others get the pages `goodfellow-next finish` writes at old addresses.
|
|
66
94
|
* - In `next dev`, runs the admin panel's local backend, so publishing saves to
|
|
67
95
|
* the files on disk. It's never part of a build.
|
|
68
96
|
* - In `next dev`, turns off React's debug channel (`experimental.reactDebugChannel`)
|
|
@@ -85,20 +113,25 @@ function withGoodfellow(nextConfig = {}, options = {}) {
|
|
|
85
113
|
};
|
|
86
114
|
if (phase !== PHASE_DEVELOPMENT_SERVER) return {
|
|
87
115
|
output: "export",
|
|
88
|
-
...config
|
|
116
|
+
...config,
|
|
117
|
+
redirects: async () => [...await siteRedirects(root), ...await nextConfig.redirects?.() ?? []]
|
|
89
118
|
};
|
|
90
119
|
const port = await startDevApi(root);
|
|
91
120
|
const rewrite = {
|
|
92
121
|
source: `${DEV_API_PREFIX}/:path*`,
|
|
93
122
|
destination: `http://127.0.0.1:${port}${DEV_API_PREFIX}/:path*`
|
|
94
123
|
};
|
|
124
|
+
const calendars = {
|
|
125
|
+
source: `/${CALENDARS_DIR}/:file(.+\\.ics)`,
|
|
126
|
+
destination: `http://127.0.0.1:${port}${DEV_API_PREFIX}/${CALENDARS_DIR}/:file`
|
|
127
|
+
};
|
|
95
128
|
return {
|
|
96
129
|
...config,
|
|
97
130
|
experimental: {
|
|
98
131
|
reactDebugChannel: false,
|
|
99
132
|
...nextConfig.experimental
|
|
100
133
|
},
|
|
101
|
-
rewrites: async () => withRewrite(await nextConfig.rewrites?.(), rewrite)
|
|
134
|
+
rewrites: async () => withRewrite(withRewrite(await nextConfig.rewrites?.(), calendars), rewrite)
|
|
102
135
|
};
|
|
103
136
|
};
|
|
104
137
|
}
|
package/dist/export.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
//#region src/export.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Writes the calendar files of the site's collections of events into the
|
|
4
|
+
* export, as `goodfellow build` does: Next.js has no route for them. Returns
|
|
5
|
+
* how many it wrote.
|
|
6
|
+
*/
|
|
7
|
+
export declare function writeCalendarFiles(outDir: string, root?: string): Promise<number>;
|
|
8
|
+
/**
|
|
9
|
+
* Writes a page at each of the site's old addresses that sends visitors on, as
|
|
10
|
+
* `goodfellow build` does. Pages win: an address the export has a page at keeps
|
|
11
|
+
* it. Returns how many it wrote.
|
|
12
|
+
*/
|
|
13
|
+
export declare function writeRedirectFiles(outDir: string, root?: string): Promise<number>;
|
|
2
14
|
/**
|
|
3
15
|
* Puts a static export's prefetch files where the browser asks for them.
|
|
4
16
|
* Next.js names each after its segment path with `/` turned into `.`
|
package/dist/export.js
CHANGED
|
@@ -1,7 +1,58 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { allPages, calendarFiles, loadSiteContent, redirectFiles } from "@goodfellow-cms/core";
|
|
2
|
+
import { fileSystemSource } from "@goodfellow-cms/core/node";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
5
|
//#region src/export.ts
|
|
4
6
|
/**
|
|
7
|
+
* Writes the calendar files of the site's collections of events into the
|
|
8
|
+
* export, as `goodfellow build` does: Next.js has no route for them. Returns
|
|
9
|
+
* how many it wrote.
|
|
10
|
+
*/
|
|
11
|
+
async function writeCalendarFiles(outDir, root = process.cwd()) {
|
|
12
|
+
const files = calendarFiles(await loadSiteContent(fileSystemSource(resolve(root))));
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
const target = join(outDir, file.path.slice(1));
|
|
15
|
+
await mkdir(dirname(target), { recursive: true });
|
|
16
|
+
await writeFile(target, file.content);
|
|
17
|
+
}
|
|
18
|
+
return files.length;
|
|
19
|
+
}
|
|
20
|
+
/** The `basePath` the export was built with, which Next.js records in `.next/routes-manifest.json`. */
|
|
21
|
+
async function exportBasePath(root) {
|
|
22
|
+
try {
|
|
23
|
+
const manifest = JSON.parse(await readFile(join(root, ".next/routes-manifest.json"), "utf8"));
|
|
24
|
+
return typeof manifest.basePath === "string" ? manifest.basePath : "";
|
|
25
|
+
} catch {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function exists(path) {
|
|
30
|
+
return access(path).then(() => true, () => false);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Writes a page at each of the site's old addresses that sends visitors on, as
|
|
34
|
+
* `goodfellow build` does. Pages win: an address the export has a page at keeps
|
|
35
|
+
* it. Returns how many it wrote.
|
|
36
|
+
*/
|
|
37
|
+
async function writeRedirectFiles(outDir, root = process.cwd()) {
|
|
38
|
+
const dir = resolve(root);
|
|
39
|
+
const content = await loadSiteContent(fileSystemSource(dir));
|
|
40
|
+
const files = redirectFiles(content, {
|
|
41
|
+
taken: allPages(content).map((page) => page.path),
|
|
42
|
+
base: await exportBasePath(dir)
|
|
43
|
+
});
|
|
44
|
+
let written = 0;
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const target = join(outDir, file.path.slice(1));
|
|
47
|
+
const page = join(outDir, `${file.path.replace(/\/index\.html$/, "")}.html`);
|
|
48
|
+
if (await exists(target) || await exists(page)) continue;
|
|
49
|
+
await mkdir(dirname(target), { recursive: true });
|
|
50
|
+
await writeFile(target, file.content);
|
|
51
|
+
written++;
|
|
52
|
+
}
|
|
53
|
+
return written;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
5
56
|
* Puts a static export's prefetch files where the browser asks for them.
|
|
6
57
|
* Next.js names each after its segment path with `/` turned into `.`
|
|
7
58
|
* (`__next.$oc$path.__PAGE__.txt`), but on Windows the path has `\` instead, so
|
|
@@ -38,4 +89,4 @@ async function fixSegmentFiles(outDir) {
|
|
|
38
89
|
return moved;
|
|
39
90
|
}
|
|
40
91
|
//#endregion
|
|
41
|
-
export { fixSegmentFiles };
|
|
92
|
+
export { fixSegmentFiles, writeCalendarFiles, writeRedirectFiles };
|
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export declare function goodfellowPages(config: GoodfellowConfig, options?: Good
|
|
|
38
38
|
path: string[];
|
|
39
39
|
}>>;
|
|
40
40
|
generateMetadata({ params }: PageProps): Promise<Metadata>;
|
|
41
|
+
/** For `app/admin/layout.tsx`: the admin panel's title, kept out of search engines, with the site's icon. */
|
|
42
|
+
adminMetadata(): Promise<Metadata>;
|
|
41
43
|
/**
|
|
42
44
|
* For `app/(site)/layout.tsx`: the site's header and footer around every page, kept as
|
|
43
45
|
* they are when moving between pages. Menus mark the current page in the browser.
|
|
@@ -60,6 +62,11 @@ export declare function goodfellowPages(config: GoodfellowConfig, options?: Good
|
|
|
60
62
|
* so other sites get an empty file.
|
|
61
63
|
*/
|
|
62
64
|
demoContent(): Promise<Response>;
|
|
65
|
+
/**
|
|
66
|
+
* For `app/goodfellow-site.json/route.ts`: what the site says about its blocks, for tools that
|
|
67
|
+
* edit its content without its code, such as the Claude connector.
|
|
68
|
+
*/
|
|
69
|
+
siteDescription(): Response;
|
|
63
70
|
/** For `app/robots.ts`: lets search engines in, and points them at the sitemap once the site's address is set. */
|
|
64
71
|
robots(): Promise<MetadataRoute.Robots>;
|
|
65
72
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { CurrentMenuLinks, NextImage, NextLink } from "./components.js";
|
|
2
|
-
import { absoluteUrl,
|
|
3
|
-
import { fileSystemSource, readMediaSizes } from "@goodfellow-cms/core/node";
|
|
4
|
-
import { BodyCode, HeadCode, PageBody, PageContent, PageFooter, PageHeader, applyPageEntry, createPuckConfigs, prepareLayout, preparePage } from "@goodfellow-cms/react";
|
|
5
|
-
import { notFound } from "next/navigation";
|
|
2
|
+
import { absoluteUrl, colorSchemeMeta, colorSchemeScript, demoContent, findRedirect, fontPreloads, getPageHead, googleFontsUrl, loadSiteContent, normalizePagePath, sitePages, themeToCss, todayIn, withBase } from "@goodfellow-cms/core";
|
|
3
|
+
import { fileSystemSource, fontStandIns, readMediaSizes, readStylesheet } from "@goodfellow-cms/core/node";
|
|
4
|
+
import { BodyCode, HeadCode, PageBody, PageContent, PageFooter, PageHeader, applyPageEntry, createPuckConfigs, describeSite, prepareLayout, preparePage } from "@goodfellow-cms/react";
|
|
5
|
+
import { notFound, redirect } from "next/navigation";
|
|
6
6
|
import { cache } from "react";
|
|
7
|
+
import { preload } from "react-dom";
|
|
7
8
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
//#region src/index.tsx
|
|
9
10
|
/** Next.js's own link and image components, for blocks' `<SiteLink>` and `<SiteImage>`. */
|
|
@@ -27,8 +28,12 @@ function pathOf(segments) {
|
|
|
27
28
|
return "";
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
/** Every page the site's export has: its pages, entries' pages and the pages blocks add, such as a calendar's months. */
|
|
32
|
+
function exportedPages(content, blocks) {
|
|
33
|
+
return sitePages(content, todayIn(content.settings.timeZone), blocks);
|
|
34
|
+
}
|
|
35
|
+
function findPage(content, blocks, path) {
|
|
36
|
+
return exportedPages(content, blocks).find((page) => page.path === path);
|
|
32
37
|
}
|
|
33
38
|
function toAbsolute(siteUrl, url) {
|
|
34
39
|
if (!url || !siteUrl || !url.startsWith("/") || url.startsWith("//")) return url;
|
|
@@ -73,39 +78,64 @@ function goodfellowPages(config, options = {}) {
|
|
|
73
78
|
media: await readMediaSizes(root)
|
|
74
79
|
});
|
|
75
80
|
/** The site's theme and fonts, which React moves into the document's head. themeToCss escapes its output. */
|
|
76
|
-
function Theme({ content }) {
|
|
81
|
+
async function Theme({ content }) {
|
|
77
82
|
const fontsUrl = googleFontsUrl(content.settings.theme);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
const [fontFiles, standIns] = await Promise.all([fontPreloads(content.settings.theme, readStylesheet), fontStandIns(content.settings.theme)]);
|
|
84
|
+
for (const href of fontFiles) preload(href, {
|
|
85
|
+
as: "font",
|
|
86
|
+
type: "font/woff2",
|
|
87
|
+
crossOrigin: ""
|
|
88
|
+
});
|
|
89
|
+
const colorScheme = colorSchemeMeta(content.settings.theme);
|
|
90
|
+
const schemeScript = colorSchemeScript(content.settings.theme);
|
|
91
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
92
|
+
colorScheme && /* @__PURE__ */ jsx("meta", {
|
|
93
|
+
name: "color-scheme",
|
|
94
|
+
content: colorScheme
|
|
86
95
|
}),
|
|
87
|
-
/* @__PURE__ */ jsx("
|
|
88
|
-
|
|
89
|
-
href: "
|
|
90
|
-
|
|
96
|
+
schemeScript && /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: schemeScript } }),
|
|
97
|
+
/* @__PURE__ */ jsx("style", {
|
|
98
|
+
href: "goodfellow-theme",
|
|
99
|
+
precedence: "default",
|
|
100
|
+
children: themeToCss(content.settings.theme, standIns)
|
|
91
101
|
}),
|
|
92
|
-
/* @__PURE__ */
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
fontsUrl && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
103
|
+
/* @__PURE__ */ jsx("link", {
|
|
104
|
+
rel: "preconnect",
|
|
105
|
+
href: "https://fonts.googleapis.com"
|
|
106
|
+
}),
|
|
107
|
+
/* @__PURE__ */ jsx("link", {
|
|
108
|
+
rel: "preconnect",
|
|
109
|
+
href: "https://fonts.gstatic.com",
|
|
110
|
+
crossOrigin: ""
|
|
111
|
+
}),
|
|
112
|
+
/* @__PURE__ */ jsx("link", {
|
|
113
|
+
rel: "stylesheet",
|
|
114
|
+
href: fontsUrl,
|
|
115
|
+
precedence: "default"
|
|
116
|
+
})
|
|
117
|
+
] })
|
|
118
|
+
] });
|
|
98
119
|
}
|
|
99
120
|
return {
|
|
100
121
|
/** Every page's address, so Next.js builds them all. Any other address is "not found". */
|
|
101
122
|
async generateStaticParams() {
|
|
102
|
-
return
|
|
123
|
+
return exportedPages(await site(), config.blocks).filter((page) => page.path !== "/404").map((page) => ({ path: page.path === "/" ? [] : page.path.slice(1).split("/") }));
|
|
103
124
|
},
|
|
104
125
|
async generateMetadata({ params }) {
|
|
105
126
|
const content = await site();
|
|
106
|
-
const page = findPage(content, pathOf((await params).path));
|
|
127
|
+
const page = findPage(content, config.blocks, pathOf((await params).path));
|
|
107
128
|
return page ? pageMetadata(content, applyPageEntry(configs, content, page), basePath()) : {};
|
|
108
129
|
},
|
|
130
|
+
/** For `app/admin/layout.tsx`: the admin panel's title, kept out of search engines, with the site's icon. */
|
|
131
|
+
async adminMetadata() {
|
|
132
|
+
const favicon = await site().then((content) => content.settings.favicon, () => void 0);
|
|
133
|
+
return {
|
|
134
|
+
title: "Site admin",
|
|
135
|
+
robots: { index: false },
|
|
136
|
+
...favicon && { icons: { icon: withBase(favicon, basePath()) } }
|
|
137
|
+
};
|
|
138
|
+
},
|
|
109
139
|
/**
|
|
110
140
|
* For `app/(site)/layout.tsx`: the site's header and footer around every page, kept as
|
|
111
141
|
* they are when moving between pages. Menus mark the current page in the browser.
|
|
@@ -134,8 +164,13 @@ function goodfellowPages(config, options = {}) {
|
|
|
134
164
|
/** The page at the route's address, inside `Layout`'s header and footer. */
|
|
135
165
|
async Page({ params }) {
|
|
136
166
|
const content = await site();
|
|
137
|
-
const
|
|
138
|
-
|
|
167
|
+
const segments = (await params).path;
|
|
168
|
+
const page = findPage(content, config.blocks, pathOf(segments));
|
|
169
|
+
if (!page || page.path === "/404") {
|
|
170
|
+
const moved = findRedirect(content, `/${(segments ?? []).join("/")}`);
|
|
171
|
+
if (moved) redirect(moved.to);
|
|
172
|
+
notFound();
|
|
173
|
+
}
|
|
139
174
|
const prepared = await preparePage(configs, content, page, await renderOptions());
|
|
140
175
|
return /* @__PURE__ */ jsx(PageContent, {
|
|
141
176
|
site: prepared.site,
|
|
@@ -149,7 +184,7 @@ function goodfellowPages(config, options = {}) {
|
|
|
149
184
|
*/
|
|
150
185
|
async NotFound() {
|
|
151
186
|
const content = await site();
|
|
152
|
-
const page = findPage(content, "/404");
|
|
187
|
+
const page = findPage(content, config.blocks, "/404");
|
|
153
188
|
if (!page) return /* @__PURE__ */ jsx("h1", { children: "Page not found" });
|
|
154
189
|
const prepared = await preparePage(configs, content, page, await renderOptions());
|
|
155
190
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -172,7 +207,7 @@ function goodfellowPages(config, options = {}) {
|
|
|
172
207
|
const content = await site();
|
|
173
208
|
const url = content.settings.url;
|
|
174
209
|
if (!url) return [];
|
|
175
|
-
return
|
|
210
|
+
return exportedPages(content, config.blocks).filter((page) => page.path !== "/404").map((page) => ({ url: absoluteUrl(url, page.path) }));
|
|
176
211
|
},
|
|
177
212
|
/**
|
|
178
213
|
* For `app/admin/demo-content.json/route.ts`: in a demo (the config's `demo`), the copy of
|
|
@@ -183,6 +218,13 @@ function goodfellowPages(config, options = {}) {
|
|
|
183
218
|
if (!config.demo) return new Response(null, { status: 404 });
|
|
184
219
|
return new Response(await demoContent(fileSystemSource(root)), { headers: { "content-type": "application/json" } });
|
|
185
220
|
},
|
|
221
|
+
/**
|
|
222
|
+
* For `app/goodfellow-site.json/route.ts`: what the site says about its blocks, for tools that
|
|
223
|
+
* edit its content without its code, such as the Claude connector.
|
|
224
|
+
*/
|
|
225
|
+
siteDescription() {
|
|
226
|
+
return Response.json(describeSite(config));
|
|
227
|
+
},
|
|
186
228
|
/** For `app/robots.ts`: lets search engines in, and points them at the sitemap once the site's address is set. */
|
|
187
229
|
async robots() {
|
|
188
230
|
const url = (await site()).settings.url;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodfellow-cms/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Goodfellow pages and the admin panel in a Next.js site.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"goodfellow",
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"nextjs",
|
|
11
11
|
"next"
|
|
12
12
|
],
|
|
13
|
-
"homepage": "https://
|
|
14
|
-
"bugs": "https://github.com/
|
|
13
|
+
"homepage": "https://goodfellow-cms.github.io/goodfellow-cms/docs/nextjs",
|
|
14
|
+
"bugs": "https://github.com/goodfellow-cms/goodfellow-cms/issues",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
18
|
-
"url": "git+https://github.com/
|
|
18
|
+
"url": "git+https://github.com/goodfellow-cms/goodfellow-cms.git",
|
|
19
19
|
"directory": "packages/next"
|
|
20
20
|
},
|
|
21
21
|
"type": "module",
|
|
@@ -53,15 +53,15 @@
|
|
|
53
53
|
"dist"
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@goodfellow-cms/core": "0.
|
|
57
|
-
"@goodfellow-cms/react": "0.
|
|
56
|
+
"@goodfellow-cms/core": "0.3.0",
|
|
57
|
+
"@goodfellow-cms/react": "0.3.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@puckeditor/core": "0.23.0",
|
|
61
61
|
"next": "^16.0.0",
|
|
62
62
|
"react": "^19.0.0",
|
|
63
63
|
"react-dom": "^19.0.0",
|
|
64
|
-
"@goodfellow-cms/admin": "0.
|
|
64
|
+
"@goodfellow-cms/admin": "0.3.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@puckeditor/core": "0.23.0",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"next": "16.3.8",
|
|
72
72
|
"react": "19.3.0",
|
|
73
73
|
"react-dom": "19.3.0",
|
|
74
|
-
"@goodfellow-cms/admin": "0.
|
|
74
|
+
"@goodfellow-cms/admin": "0.3.0"
|
|
75
75
|
},
|
|
76
76
|
"engines": {
|
|
77
77
|
"node": ">=22"
|