@goodfellow-cms/next 0.3.0 → 0.4.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 CHANGED
@@ -1,12 +1,12 @@
1
1
  # @goodfellow-cms/next
2
2
 
3
- Goodfellow pages and the admin panel in a Next.js App Router site, exported as static files. Links use `next/link` and images `next/image`.
3
+ Goodfellow pages and the admin panel in a Next.js App Router site, exported as static files, or with a server of its own that publishes for editors who sign in with the site's own sign-in (`@goodfellow-cms/next/server`). Links use `next/link` and images `next/image`.
4
4
 
5
5
  Start from the Next.js starter: `npm create goodfellow@latest my-site -- --template next`.
6
6
 
7
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://goodfellow-cms.github.io/goodfellow-cms/docs/nextjs).
9
+ More in the docs: [Next.js](https://goodfellow-cms.github.io/goodfellow-cms/docs/nextjs) and [Full-stack Next.js sites](https://goodfellow-cms.github.io/goodfellow-cms/docs/full-stack-nextjs).
10
10
 
11
11
  ## License
12
12
 
@@ -2,13 +2,32 @@
2
2
  // After `next build`: `goodfellow-next finish out` fixes the export's prefetch files on Windows,
3
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
4
  // `goodfellow-next update` updates Goodfellow, as `goodfellow update` does for other sites.
5
+ // `goodfellow-next content link|copy [folder]` puts a content repository's files in place, for a site whose
6
+ // content is kept in a repository of its own.
7
+ import { rm } from "node:fs/promises";
8
+ import { relative } from "node:path";
5
9
  import { parseArgs } from "node:util";
6
10
  import { formatUpdateResult } from "@goodfellow-cms/core";
7
- import { update } from "@goodfellow-cms/core/node";
8
- import { writeSearchIndex } from "@goodfellow-cms/core/search-index";
9
- import { fixSegmentFiles, writeCalendarFiles, writeRedirectFiles } from "@goodfellow-cms/next/export";
11
+ import { ContentRepositoryError, setUpContentRepository, update } from "@goodfellow-cms/core/node";
12
+ import { writePagesSearchIndex, writeSearchIndex } from "@goodfellow-cms/core/search-index";
13
+ import { builtPages, fixSegmentFiles, writeCalendarFiles, writeRedirectFiles } from "@goodfellow-cms/next/export";
10
14
 
11
15
  const [command, dir = "out"] = process.argv.slice(2);
16
+ // A full-stack site (withGoodfellow(config, { server: true })): Next.js serves its pages and sends redirects
17
+ // itself, so the calendar files and the search index are written into public/, which `next start` and hosts
18
+ // serve as static files, as they are for an export.
19
+ if (command === "finish" && dir === "--server") {
20
+ await rm("public/calendars", { recursive: true, force: true });
21
+ const calendars = await writeCalendarFiles("public");
22
+ if (calendars > 0) console.log(`Wrote ${calendars} calendar files, in public/calendars`);
23
+ const indexed = await writePagesSearchIndex(await builtPages(), "public/pagefind");
24
+ console.log(
25
+ indexed === undefined
26
+ ? "No page has a search block, so there's nothing to index"
27
+ : `Indexed ${indexed} pages for search, in public/pagefind`,
28
+ );
29
+ process.exit(0);
30
+ }
12
31
  if (command === "update") {
13
32
  const { values } = parseArgs({
14
33
  args: process.argv.slice(3),
@@ -19,19 +38,45 @@ if (command === "update") {
19
38
  console.log(formatUpdateResult(result));
20
39
  process.exit(result.state === "failed" ? 1 : 0);
21
40
  }
41
+ if (command === "content") {
42
+ const [action, from] = process.argv.slice(3);
43
+ if (action === "link" || action === "copy") {
44
+ try {
45
+ const folder =
46
+ relative(process.cwd(), await setUpContentRepository(".", { from, copy: action === "copy" })) || ".";
47
+ console.log(
48
+ action === "copy"
49
+ ? `Copied content/ and public/media/ from ${folder}`
50
+ : `Linked content/ and public/media/ to ${folder}, so the site's content is the content repository's`,
51
+ );
52
+ process.exit(0);
53
+ } catch (error) {
54
+ if (!(error instanceof ContentRepositoryError)) throw error;
55
+ console.error(error.message);
56
+ process.exit(1);
57
+ }
58
+ }
59
+ }
22
60
  if (command !== "finish" && command !== "index") {
23
61
  console.error(
24
62
  [
25
63
  "Usage: goodfellow-next finish [folder]",
64
+ " goodfellow-next finish --server",
26
65
  " goodfellow-next index [folder]",
27
66
  " goodfellow-next update [--to fixes|automatic|latest|<release>] [--publish]",
67
+ " goodfellow-next content link|copy [folder]",
28
68
  "",
29
69
  "finish Gets the exported site in folder (default: out) ready to publish: puts the files",
30
70
  " Next.js uses to load pages ahead where browsers look for them, writes calendar files",
31
71
  " for collections of events and pages that send visitors on from old addresses, and",
32
- " indexes it for search.",
72
+ " indexes it for search. With --server, for a full-stack site, writes the calendar",
73
+ " files into public/calendars and indexes the pages next build rendered into",
74
+ " public/pagefind instead.",
33
75
  "index Only indexes the exported site for its search block.",
34
76
  "update Updates Goodfellow, and blocks from its registry, if the site still builds.",
77
+ "content For a site whose content is kept in a repository of its own: links content/ and",
78
+ " public/media/ to the content repository in folder (default: content-repository),",
79
+ " or copies them from it, as builds do.",
35
80
  ].join("\n"),
36
81
  );
37
82
  process.exit(1);
package/dist/admin.d.ts CHANGED
@@ -10,12 +10,19 @@ export interface GoodfellowAdminProps {
10
10
  siteUrl?: string;
11
11
  /** Replaces any of the admin panel's text, for rewording or translation. */
12
12
  strings?: Partial<Strings>;
13
+ /**
14
+ * On a full-stack site, the address of its `goodfellowApi()` route, such as
15
+ * `/api/goodfellow`, without the base path. Editors then sign in with the
16
+ * site's own sign-in, and its server publishes for them. `next dev` still
17
+ * saves to the files on disk.
18
+ */
19
+ server?: string;
13
20
  }
14
21
  /**
15
22
  * The admin panel, for a Next.js site's `app/admin/page.tsx`, which must be a
16
23
  * Client Component (`"use client"`) that imports the site's config.
17
24
  * In `next dev` it saves to the files on disk; on the live site, editors sign
18
- * in to the config's git backend.
25
+ * in to the config's git backend, or on a full-stack site (`server`), with the site's own sign-in.
19
26
  */
20
- export declare function GoodfellowAdmin({ config, siteUrl, strings }: GoodfellowAdminProps): import("react").JSX.Element | null;
27
+ export declare function GoodfellowAdmin({ config, siteUrl, strings, server }: GoodfellowAdminProps): import("react").JSX.Element | null;
21
28
  //#endregion
package/dist/admin.js CHANGED
@@ -40,9 +40,9 @@ async function setUp(siteUrl) {
40
40
  * The admin panel, for a Next.js site's `app/admin/page.tsx`, which must be a
41
41
  * Client Component (`"use client"`) that imports the site's config.
42
42
  * In `next dev` it saves to the files on disk; on the live site, editors sign
43
- * in to the config's git backend.
43
+ * in to the config's git backend, or on a full-stack site (`server`), with the site's own sign-in.
44
44
  */
45
- function GoodfellowAdmin({ config, siteUrl = `${BASE_PATH}/`, strings }) {
45
+ function GoodfellowAdmin({ config, siteUrl = `${BASE_PATH}/`, strings, server }) {
46
46
  const [setup, setSetup] = useState(null);
47
47
  useEffect(() => {
48
48
  let current = true;
@@ -64,6 +64,7 @@ function GoodfellowAdmin({ config, siteUrl = `${BASE_PATH}/`, strings }) {
64
64
  store: setup.store,
65
65
  registry: `${BASE_PATH}/__goodfellow/api/registry/{name}.json`
66
66
  },
67
+ ...server && { server: `${BASE_PATH}${server}` },
67
68
  preview: {
68
69
  stylesheets: setup.stylesheets,
69
70
  themeCss: theme_css_default,
package/dist/config.d.ts CHANGED
@@ -3,6 +3,13 @@ import { NextConfig } from "next";
3
3
  export interface GoodfellowNextOptions {
4
4
  /** The site's folder, holding `content/` and `public/media/`. Defaults to the folder Next.js runs in. */
5
5
  root?: string;
6
+ /**
7
+ * For a full-stack site, with a server of its own: builds it for `next start`
8
+ * or a host that runs Next.js, rather than as static files, and includes
9
+ * `content/` and `public/media/` with its server, which reads them for pages
10
+ * built on request and calendar files.
11
+ */
12
+ server?: boolean;
6
13
  }
7
14
  /**
8
15
  * Sets up Next.js for a Goodfellow site, in `next.config.ts`:
@@ -16,6 +23,8 @@ export interface GoodfellowNextOptions {
16
23
  * - Serves images as they are unless the site sets an image loader, since static files can't be optimized on request.
17
24
  * - Adds the site's redirects (Site Settings → Redirects) ahead of the site's own, for hosts that
18
25
  * send them, such as Vercel. Others get the pages `goodfellow-next finish` writes at old addresses.
26
+ * - With `{ server: true }`, builds a full-stack site for a server instead of as static
27
+ * files: see `goodfellowApi()` in `@goodfellow-cms/next/server`.
19
28
  * - In `next dev`, runs the admin panel's local backend, so publishing saves to
20
29
  * the files on disk. It's never part of a build.
21
30
  * - In `next dev`, turns off React's debug channel (`experimental.reactDebugChannel`)
package/dist/config.js CHANGED
@@ -1,5 +1,5 @@
1
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";
2
+ import { DEV_API_PREFIX, contentRepositoryProblem, 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
@@ -91,6 +91,8 @@ async function siteRedirects(root) {
91
91
  * - Serves images as they are unless the site sets an image loader, since static files can't be optimized on request.
92
92
  * - Adds the site's redirects (Site Settings → Redirects) ahead of the site's own, for hosts that
93
93
  * send them, such as Vercel. Others get the pages `goodfellow-next finish` writes at old addresses.
94
+ * - With `{ server: true }`, builds a full-stack site for a server instead of as static
95
+ * files: see `goodfellowApi()` in `@goodfellow-cms/next/server`.
94
96
  * - In `next dev`, runs the admin panel's local backend, so publishing saves to
95
97
  * the files on disk. It's never part of a build.
96
98
  * - In `next dev`, turns off React's debug channel (`experimental.reactDebugChannel`)
@@ -101,6 +103,8 @@ function withGoodfellow(nextConfig = {}, options = {}) {
101
103
  const root = resolve(options.root ?? process.cwd());
102
104
  const basePath = nextBasePath(nextConfig.basePath ?? process.env.GOODFELLOW_BASE);
103
105
  return async (phase) => {
106
+ const problem = contentRepositoryProblem(root);
107
+ if (problem) throw new Error(problem);
104
108
  const config = {
105
109
  trailingSlash: true,
106
110
  ...nextConfig,
@@ -111,6 +115,19 @@ function withGoodfellow(nextConfig = {}, options = {}) {
111
115
  GOODFELLOW_BASE_PATH: basePath
112
116
  }
113
117
  };
118
+ if (phase !== PHASE_DEVELOPMENT_SERVER && options.server) return {
119
+ ...config,
120
+ images: nextConfig.images,
121
+ outputFileTracingIncludes: {
122
+ ...nextConfig.outputFileTracingIncludes,
123
+ "/**": [
124
+ ...nextConfig.outputFileTracingIncludes?.["/**"] ?? [],
125
+ "./content/**/*",
126
+ "./public/media/**/*"
127
+ ]
128
+ },
129
+ redirects: async () => [...await siteRedirects(root), ...await nextConfig.redirects?.() ?? []]
130
+ };
114
131
  if (phase !== PHASE_DEVELOPMENT_SERVER) return {
115
132
  output: "export",
116
133
  ...config,
package/dist/export.d.ts CHANGED
@@ -19,4 +19,16 @@ export declare function writeRedirectFiles(outDir: string, root?: string): Promi
19
19
  * prefetch is "not found". Returns how many files it moved; none elsewhere.
20
20
  */
21
21
  export declare function fixSegmentFiles(outDir: string): Promise<number>;
22
+ /** A page `next build` rendered ahead: its address on the site, and its HTML. */
23
+ export interface BuiltPage {
24
+ url: string;
25
+ html: string;
26
+ }
27
+ /**
28
+ * The pages `next build` rendered ahead for a full-stack site (`withGoodfellow(config,
29
+ * { server: true })`), at their addresses on the site, without its base path, as the
30
+ * Search block looks them up. Next.js lists them in `.next/prerender-manifest.json`
31
+ * and keeps their HTML in `.next/server/app/`. The admin panel and Next.js's own pages are left out.
32
+ */
33
+ export declare function builtPages(root?: string): Promise<BuiltPage[]>;
22
34
  //#endregion
package/dist/export.js CHANGED
@@ -88,5 +88,28 @@ async function fixSegmentFiles(outDir) {
88
88
  await visit(outDir);
89
89
  return moved;
90
90
  }
91
+ /**
92
+ * The pages `next build` rendered ahead for a full-stack site (`withGoodfellow(config,
93
+ * { server: true })`), at their addresses on the site, without its base path, as the
94
+ * Search block looks them up. Next.js lists them in `.next/prerender-manifest.json`
95
+ * and keeps their HTML in `.next/server/app/`. The admin panel and Next.js's own pages are left out.
96
+ */
97
+ async function builtPages(root = process.cwd()) {
98
+ const next = join(resolve(root), ".next");
99
+ const manifest = JSON.parse(await readFile(join(next, "prerender-manifest.json"), "utf8"));
100
+ const trailingSlash = JSON.parse(await readFile(join(next, "required-server-files.json"), "utf8")).config?.trailingSlash === true;
101
+ const pages = [];
102
+ for (const route of Object.keys(manifest.routes ?? {})) {
103
+ if (route.startsWith("/_") || route === "/admin" || route.startsWith("/admin/")) continue;
104
+ const file = join(next, "server/app", `${route === "/" ? "/index" : route}.html`);
105
+ if (!await exists(file)) continue;
106
+ const url = route === "/" ? "/" : trailingSlash ? `${route}/` : route;
107
+ pages.push({
108
+ url,
109
+ html: await readFile(file, "utf8")
110
+ });
111
+ }
112
+ return pages;
113
+ }
91
114
  //#endregion
92
- export { fixSegmentFiles, writeCalendarFiles, writeRedirectFiles };
115
+ export { builtPages, fixSegmentFiles, writeCalendarFiles, writeRedirectFiles };
@@ -0,0 +1,57 @@
1
+ import { GoodfellowConfig } from "@goodfellow-cms/core";
2
+ import { AppApiOptions } from "@goodfellow-cms/core/node";
3
+ import { ServerKeys } from "@goodfellow-cms/ai";
4
+ //#region src/server.d.ts
5
+ /** The environment variable the server's token is read from, unless `goodfellowApi({ token })` gives one. */
6
+ export declare const GIT_TOKEN_VARIABLE = "GOODFELLOW_GIT_TOKEN";
7
+ export interface GoodfellowApiOptions {
8
+ /** The site's `goodfellow.config.tsx`, whose `backend` says where the site is stored. */
9
+ config: GoodfellowConfig;
10
+ /**
11
+ * A token that can read and publish to the site's repository, such as a
12
+ * fine-grained GitHub token or a GitLab project access token. It never
13
+ * reaches a browser. Defaults to the `GOODFELLOW_GIT_TOKEN` environment variable.
14
+ */
15
+ token?: string | (() => Promise<string>);
16
+ /**
17
+ * Who's signed in, from the site's own sign-in (such as Better Auth's
18
+ * `auth.api.getSession({ headers: request.headers })`): their name, email
19
+ * address and role, or `null` if nobody is, or `false` if they may not edit.
20
+ */
21
+ authorize: AppApiOptions["authorize"];
22
+ /** Where the admin panel sends people to sign in, such as `/sign-in?next=/admin/`. */
23
+ signIn: string;
24
+ /** Where the admin panel's Sign out button goes, if the site has an address for it. */
25
+ signOut?: string;
26
+ /**
27
+ * Keys for AI services, such as `{ anthropic: process.env.ANTHROPIC_API_KEY }`,
28
+ * so the AI assistant's requests go through the server and editors need no
29
+ * keys of their own. Services without a key are left out.
30
+ */
31
+ ai?: ServerKeys;
32
+ /** Told about failures editors only hear about as "something went wrong". Defaults to `console.error`. */
33
+ onError?: (error: unknown) => void;
34
+ }
35
+ /** The route parameters of a catch-all route named `[...path]`. */
36
+ interface RouteContext {
37
+ params: Promise<{
38
+ path?: string[];
39
+ }>;
40
+ }
41
+ /**
42
+ * The admin panel's API on a full-stack Next.js site, for
43
+ * `app/api/goodfellow/[...path]/route.ts`:
44
+ *
45
+ * ```ts
46
+ * export const { GET, POST } = goodfellowApi({ config, authorize, signIn: "/sign-in" });
47
+ * ```
48
+ *
49
+ * The admin panel (`<GoodfellowAdmin server="/api/goodfellow" />`) reads and
50
+ * publishes through it, and the server reaches the repository with its own
51
+ * token, naming each editor as the author of what they publish.
52
+ */
53
+ export declare function goodfellowApi(options: GoodfellowApiOptions): {
54
+ GET: (request: Request, context: RouteContext) => Promise<Response>;
55
+ POST: (request: Request, context: RouteContext) => Promise<Response>;
56
+ };
57
+ //#endregion
package/dist/server.js ADDED
@@ -0,0 +1,45 @@
1
+ import { appApi } from "@goodfellow-cms/core/node";
2
+ import { aiServer } from "@goodfellow-cms/ai";
3
+ //#region src/server.ts
4
+ /** The environment variable the server's token is read from, unless `goodfellowApi({ token })` gives one. */
5
+ const GIT_TOKEN_VARIABLE = "GOODFELLOW_GIT_TOKEN";
6
+ /**
7
+ * The admin panel's API on a full-stack Next.js site, for
8
+ * `app/api/goodfellow/[...path]/route.ts`:
9
+ *
10
+ * ```ts
11
+ * export const { GET, POST } = goodfellowApi({ config, authorize, signIn: "/sign-in" });
12
+ * ```
13
+ *
14
+ * The admin panel (`<GoodfellowAdmin server="/api/goodfellow" />`) reads and
15
+ * publishes through it, and the server reaches the repository with its own
16
+ * token, naming each editor as the author of what they publish.
17
+ */
18
+ function goodfellowApi(options) {
19
+ let api;
20
+ function handler() {
21
+ if (api) return api;
22
+ const { backend } = options.config;
23
+ if (!backend?.server) throw new Error("goodfellowApi() needs the config's `backend`, such as github({ repo }) or gitlab({ project }).");
24
+ const token = options.token || process.env["GOODFELLOW_GIT_TOKEN"] || (() => {
25
+ throw new Error(`goodfellowApi() needs a token: set GOODFELLOW_GIT_TOKEN, or pass \`token\`.`);
26
+ })();
27
+ const ai = options.ai && aiServer(options.ai);
28
+ api = appApi({
29
+ git: backend.server({ token }),
30
+ authorize: options.authorize,
31
+ signIn: options.signIn,
32
+ ...options.signOut && { signOut: options.signOut },
33
+ ...ai && ai.services.length > 0 && { ai },
34
+ ...options.onError && { onError: options.onError }
35
+ });
36
+ return api;
37
+ }
38
+ const route = async (request, context) => handler()(request, ((await context.params).path ?? []).join("/"));
39
+ return {
40
+ GET: route,
41
+ POST: route
42
+ };
43
+ }
44
+ //#endregion
45
+ export { GIT_TOKEN_VARIABLE, goodfellowApi };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodfellow-cms/next",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Goodfellow pages and the admin panel in a Next.js site.",
5
5
  "keywords": [
6
6
  "goodfellow",
@@ -43,6 +43,11 @@
43
43
  "import": "./dist/export.js",
44
44
  "default": "./dist/export.js"
45
45
  },
46
+ "./server": {
47
+ "types": "./dist/server.d.ts",
48
+ "import": "./dist/server.js",
49
+ "default": "./dist/server.js"
50
+ },
46
51
  "./package.json": "./package.json"
47
52
  },
48
53
  "bin": {
@@ -53,15 +58,16 @@
53
58
  "dist"
54
59
  ],
55
60
  "dependencies": {
56
- "@goodfellow-cms/core": "0.3.0",
57
- "@goodfellow-cms/react": "0.3.0"
61
+ "@goodfellow-cms/ai": "0.4.0",
62
+ "@goodfellow-cms/react": "0.4.0",
63
+ "@goodfellow-cms/core": "0.4.0"
58
64
  },
59
65
  "peerDependencies": {
60
66
  "@puckeditor/core": "0.23.0",
61
67
  "next": "^16.0.0",
62
68
  "react": "^19.0.0",
63
69
  "react-dom": "^19.0.0",
64
- "@goodfellow-cms/admin": "0.3.0"
70
+ "@goodfellow-cms/admin": "0.4.0"
65
71
  },
66
72
  "devDependencies": {
67
73
  "@puckeditor/core": "0.23.0",
@@ -71,7 +77,9 @@
71
77
  "next": "16.3.8",
72
78
  "react": "19.3.0",
73
79
  "react-dom": "19.3.0",
74
- "@goodfellow-cms/admin": "0.3.0"
80
+ "@goodfellow-cms/admin": "0.4.0",
81
+ "@goodfellow-cms/github": "0.4.0",
82
+ "@goodfellow-cms/gitlab": "0.4.0"
75
83
  },
76
84
  "engines": {
77
85
  "node": ">=22"