@goodfellow-cms/next 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,12 +3,14 @@
3
3
  // writes the calendar files of collections of events, collections' feeds, the pages at redirects' old addresses and Cloudflare Pages' _redirects, and the search index for sites with a search block,
4
4
  // copies the site's Google Fonts into the export, builds plugins' admin frames and runs their build steps, starts every page with the admin address's guard for a site with one, and reports what automated checks find on the exported pages. `index` only writes the index.
5
5
  // `goodfellow-next update` updates Goodfellow, as `goodfellow update` does for other sites.
6
- // `goodfellow-next task [--out <file>]` runs one of a plugin's tasks for the admin panel, as `goodfellow task` does.
6
+ // `goodfellow-next task [--describe] [--out <file>]` runs one of a plugin's tasks for the admin panel, or with
7
+ // --describe only writes it down for a job with no secrets, as `goodfellow task` does.
7
8
  // `goodfellow-next due [--since <time>] [--output <file>]` says whether a scheduled change has come due, as `goodfellow due` does.
8
9
  // `goodfellow-next nightly [--mode <mode>]` says whether tonight's build is needed, as `goodfellow nightly` does.
9
10
  // `goodfellow-next content link|copy [folder]` puts a content repository's files in place, for a site whose
10
11
  // content is kept in a repository of its own.
11
12
  // `goodfellow-next import wordpress <file or address> [--dry-run]` imports a WordPress site, as `goodfellow import` does.
13
+ // `goodfellow-next import markdown <folder> [--dry-run]` imports a folder of Markdown files, as `goodfellow import` does.
12
14
  // `goodfellow-next export wordpress [file]` writes the site as a WordPress export file, as `goodfellow export` does.
13
15
  // `goodfellow-next blocks add <item> [--registry <folder>] [--native <app>]` makes a block of a shadcn item, as
14
16
  // `goodfellow blocks add` does, and with --native its version for an Expo app.
@@ -16,7 +18,7 @@
16
18
  // the token broker, in goodfellow.config.tsx, as `goodfellow admin` and `goodfellow broker` do.
17
19
  import { existsSync, readFileSync } from "node:fs";
18
20
  import { rm } from "node:fs/promises";
19
- import { relative, resolve } from "node:path";
21
+ import { join, relative, resolve } from "node:path";
20
22
  import { parseArgs } from "node:util";
21
23
  import { buildNotes, CLOUDFLARE_REDIRECTS_LIMIT, configAdmin, formatUpdateResult } from "@goodfellow-cms/core";
22
24
  import { reportSiteProblems } from "@goodfellow-cms/core/accessibility";
@@ -24,13 +26,19 @@ import {
24
26
  AdminSetupError,
25
27
  BrokerSetupError,
26
28
  ContentRepositoryError,
29
+ checkEarlyTheme,
27
30
  checkSiteAdminAddress,
28
31
  contentRepositoryProblem,
29
32
  exportWordPress,
30
33
  formatAdminResult,
34
+ formatAdminText,
35
+ formatMarkdownImport,
31
36
  formatWordPressExport,
32
37
  formatWordPressImport,
38
+ importMarkdown,
33
39
  importWordPress,
40
+ MarkdownFolderError,
41
+ markdownFolderChoices,
34
42
  nightlyCheck,
35
43
  reportDue,
36
44
  reportNightly,
@@ -39,6 +47,8 @@ import {
39
47
  setBrokerAddress,
40
48
  setUpContentRepository,
41
49
  update,
50
+ writeAdminText,
51
+ writeTaskDescription,
42
52
  } from "@goodfellow-cms/core/node";
43
53
  import { writePagesSearchIndex, writeSearchIndex } from "@goodfellow-cms/core/search-index";
44
54
  import { addShadcnBlock, blockWorks, formatShadcnBlock, ShadcnBlockError } from "@goodfellow-cms/core/shadcn";
@@ -49,9 +59,11 @@ import {
49
59
  builtPages,
50
60
  checkPages,
51
61
  exportedPages,
62
+ exportTrailingSlash,
52
63
  fixSegmentFiles,
53
64
  guardExport,
54
65
  NEXT_SITE_FILES,
66
+ nextDistDir,
55
67
  writeBuildRecordFile,
56
68
  writeCalendarFiles,
57
69
  writeContentApi,
@@ -71,6 +83,19 @@ const CONFIG_FILES = ["goodfellow.config.tsx", "goodfellow.config.ts", "goodfell
71
83
  // A full-stack site (withGoodfellow(config, { server: true })): Next.js serves its pages and sends redirects
72
84
  // itself, so the calendar files, feeds and the search index are written into public/, which `next start` and hosts
73
85
  // serve as static files, as they are for an export.
86
+ // A theme installed before it was three days old is used only once its provenance checks out, whatever the
87
+ // admin panel said, since a browser can lie.
88
+ if (command === "finish") {
89
+ // Pages' full addresses end as Next.js served the pages, unless where the site is built says otherwise.
90
+ process.env.GOODFELLOW_TRAILING_SLASH ||= String(await exportTrailingSlash());
91
+ try {
92
+ const theme = await checkEarlyTheme(process.cwd());
93
+ if (theme) console.log(`${theme.name} ${theme.release} was installed early: its provenance checks out.`);
94
+ } catch (error) {
95
+ console.error(error instanceof Error ? error.message : error);
96
+ process.exit(1);
97
+ }
98
+ }
74
99
  if (command === "finish" && dir === "--server") {
75
100
  // Next.js renders a full-stack site's pages as it serves them, so nothing can put a guard first in each.
76
101
  const config = CONFIG_FILES.find((file) => existsSync(file));
@@ -82,13 +107,15 @@ if (command === "finish" && dir === "--server") {
82
107
  }
83
108
  for (const folder of await buildFileFolders()) await rm(`public/${folder}`, { recursive: true, force: true });
84
109
  // The editor's frames' page, which next start serves as it was rendered.
85
- await writeEditorPolicy(".next/server/app");
110
+ await writeEditorPolicy(join(await nextDistDir(), "server/app"));
86
111
  const calendars = await writeCalendarFiles("public");
87
112
  if (calendars > 0) console.log(`Wrote ${calendars} calendar files, in public/calendars`);
88
113
  const feeds = await writeFeedFiles("public");
89
114
  if (feeds > 0) console.log(`Wrote ${feeds} feeds, in public/feeds`);
90
115
  const api = await writeContentApi("public");
91
116
  if (api !== undefined) console.log(`Wrote the content API, with ${api} pages, in public/api`);
117
+ // The admin panel's text in the site's own languages, for the sign-in screen, which can't read the site yet.
118
+ for (const line of formatAdminText(await writeAdminText("public"))) console.log(line);
92
119
  // Before the pages are read, so the search index and the checks see each page's language.
93
120
  const languages = await writePageLanguages(undefined);
94
121
  if (languages > 0) console.log(`Marked ${languages} pages with their language`);
@@ -119,8 +146,20 @@ if (command === "update") {
119
146
  process.exit(result.state === "failed" ? 1 : 0);
120
147
  }
121
148
  if (command === "task") {
122
- const { values } = parseArgs({ args: process.argv.slice(3), options: { out: { type: "string" } } });
149
+ const { values } = parseArgs({
150
+ args: process.argv.slice(3),
151
+ options: { out: { type: "string" }, describe: { type: "boolean" } },
152
+ });
123
153
  const env = process.env;
154
+ if (values.describe) {
155
+ await writeTaskDescription(
156
+ ".",
157
+ { module: new URL("../dist/export.js", import.meta.url).href, export: "loadTaskConfig" },
158
+ { plugin: env.GOODFELLOW_TASK_PLUGIN ?? "", task: env.GOODFELLOW_TASK_NAME ?? "" },
159
+ values.out,
160
+ );
161
+ process.exit(0);
162
+ }
124
163
  await writeTaskResult(
125
164
  ".",
126
165
  {
@@ -204,10 +243,39 @@ if (command === "import") {
204
243
  const { positionals, values } = parseArgs({
205
244
  args: process.argv.slice(3),
206
245
  allowPositionals: true,
207
- options: { "dry-run": { type: "boolean" } },
246
+ options: {
247
+ "dry-run": { type: "boolean" },
248
+ pages: { type: "string", multiple: true },
249
+ collection: { type: "string", multiple: true },
250
+ "replace-home": { type: "boolean" },
251
+ "replace-pages": { type: "boolean" },
252
+ drafts: { type: "boolean" },
253
+ menu: { type: "boolean" },
254
+ "keep-fields": { type: "boolean" },
255
+ },
208
256
  });
209
257
  const [kind, from] = positionals;
210
258
  const problem = contentRepositoryProblem(".");
259
+ if (kind === "markdown" && from && !problem) {
260
+ try {
261
+ const result = await importMarkdown(".", from, {
262
+ folders: markdownFolderChoices(values.pages, values.collection),
263
+ replaceHome: values["replace-home"],
264
+ replacePages: values["replace-pages"],
265
+ drafts: values.drafts,
266
+ menu: values.menu,
267
+ keepFields: values["keep-fields"],
268
+ dryRun: values["dry-run"],
269
+ log: (line) => console.log(line),
270
+ });
271
+ console.log(formatMarkdownImport(result));
272
+ process.exit(0);
273
+ } catch (error) {
274
+ if (!(error instanceof MarkdownFolderError)) throw error;
275
+ console.error(error.message);
276
+ process.exit(1);
277
+ }
278
+ }
211
279
  if (kind === "wordpress" && from && !problem) {
212
280
  try {
213
281
  const result = await importWordPress(".", from, { dryRun: values["dry-run"], log: (line) => console.log(line) });
@@ -272,6 +340,8 @@ if (command !== "finish" && command !== "index") {
272
340
  " goodfellow-next nightly [--mode changed|always|off] [--output <file>]",
273
341
  " goodfellow-next content link|copy [folder]",
274
342
  " goodfellow-next import wordpress <file or address> [--dry-run]",
343
+ " goodfellow-next import markdown <folder> [--dry-run] [--pages <folder>] [--collection <folder>]",
344
+ " [--replace-home] [--replace-pages] [--drafts] [--menu] [--keep-fields]",
275
345
  " goodfellow-next export wordpress [file]",
276
346
  " goodfellow-next blocks add <item> [--registry <folder>] [--native <app>]",
277
347
  " goodfellow-next admin add <address> | check | remove",
@@ -301,7 +371,9 @@ if (command !== "finish" && command !== "index") {
301
371
  " or copies them from it, as builds do.",
302
372
  "import Brings a WordPress site's pages, posts, categories, tags, menus and media into",
303
373
  " this site, from its export file (Tools → Export) or its address. With --dry-run,",
304
- " says what it would bring across and changes nothing.",
374
+ " says what it would bring across and changes nothing. import markdown brings a folder",
375
+ " of Markdown files in: the files at the top as pages, each folder as a collection,",
376
+ " unless --pages or --collection names it (. for the top).",
305
377
  "export Writes this site as a WordPress export file (default: named after the site),",
306
378
  " which WordPress imports with Tools → Import → WordPress.",
307
379
  "blocks Installs a shadcn registry item (button, @acme/hero, or an item's address) with",
@@ -330,6 +402,7 @@ if (command === "finish") {
330
402
  if (feeds > 0) console.log(`Wrote ${feeds} feeds`);
331
403
  const api = await writeContentApi(dir);
332
404
  if (api !== undefined) console.log(`Wrote the content API, with ${api} pages, in api/`);
405
+ for (const line of formatAdminText(await writeAdminText(dir))) console.log(line);
333
406
  const languages = await writePageLanguages(dir);
334
407
  if (languages > 0) console.log(`Marked ${languages} pages with their language`);
335
408
  // Before the redirects' pages, which aren't the site's.
package/dist/admin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { n as EDITOR_PAGE, t as ADMIN_PAGE } from "./site-files-BeLnLxCl.js";
2
+ import { n as EDITOR_PAGE, t as ADMIN_PAGE } from "./site-files-Cmu3ovIs.js";
3
3
  import { ADMIN_POLICY, DEV_EDITOR_PREFIX } from "@goodfellow-cms/core";
4
4
  import { useEffect, useState } from "react";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -16,6 +16,13 @@ export declare function NextImage({ src, alt, width, height, loading, ...props }
16
16
  export declare function CurrentMenuLinks({ basePath }: {
17
17
  basePath?: string;
18
18
  }): null;
19
+ /**
20
+ * Counts each page visitors move to with GoatCounter, whose script counts only
21
+ * the page it loads on. Plausible's and Umami's scripts follow Next.js's
22
+ * navigations themselves. Does nothing until the script has loaded, which on a
23
+ * site that asks visitors first is once they allow statistics.
24
+ */
25
+ export declare function GoatCounterNavigation(): null;
19
26
  /**
20
27
  * Opens the Search box's results with Next.js's navigation, and loads them
21
28
  * ahead when they're pointed at or reached with the keyboard, as `next/link`
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { CurrentPathProvider } from "@goodfellow-cms/react";
3
3
  import { usePathname, useRouter } from "next/navigation";
4
- import { useEffect } from "react";
4
+ import { useEffect, useRef } from "react";
5
5
  import Image from "next/image";
6
6
  import Link from "next/link";
7
7
  import { jsx } from "react/jsx-runtime";
@@ -51,6 +51,24 @@ function CurrentMenuLinks({ basePath = "" }) {
51
51
  return null;
52
52
  }
53
53
  /**
54
+ * Counts each page visitors move to with GoatCounter, whose script counts only
55
+ * the page it loads on. Plausible's and Umami's scripts follow Next.js's
56
+ * navigations themselves. Does nothing until the script has loaded, which on a
57
+ * site that asks visitors first is once they allow statistics.
58
+ */
59
+ function GoatCounterNavigation() {
60
+ const pathname = usePathname();
61
+ const first = useRef(true);
62
+ useEffect(() => {
63
+ if (first.current) {
64
+ first.current = false;
65
+ return;
66
+ }
67
+ window.goatcounter?.count?.({ path: location.pathname + location.search });
68
+ }, [pathname]);
69
+ return null;
70
+ }
71
+ /**
54
72
  * Opens the Search box's results with Next.js's navigation, and loads them
55
73
  * ahead when they're pointed at or reached with the keyboard, as `next/link`
56
74
  * does. The Search box writes its results itself (`searchScript()`), as plain
@@ -117,4 +135,4 @@ function ByLanguage({ variants }) {
117
135
  });
118
136
  }
119
137
  //#endregion
120
- export { ByLanguage, CurrentMenuLinks, NextImage, NextLink, PageLanguage, SearchResultLinks };
138
+ export { ByLanguage, CurrentMenuLinks, GoatCounterNavigation, NextImage, NextLink, PageLanguage, SearchResultLinks };
package/dist/config.d.ts CHANGED
@@ -11,6 +11,14 @@ export interface GoodfellowNextOptions {
11
11
  */
12
12
  server?: boolean;
13
13
  }
14
+ /**
15
+ * A redirect's target as Next.js's destination: Next.js reads `:name`, `*`,
16
+ * `+`, `(`, `)`, `{` and `}` in its path and hash as a pattern, which would
17
+ * fail the build (`/a/:b` names a value the source doesn't have), so they're
18
+ * percent-encoded there, which browsers and servers read as the same address.
19
+ * Next.js escapes its query values itself.
20
+ */
21
+ export declare function redirectDestination(to: string): string;
14
22
  /**
15
23
  * Sets up Next.js for a Goodfellow site, in `next.config.ts`:
16
24
  * `export default withGoodfellow({ ... })`.
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
- import { n as IMAGE_SIZES_MARK, t as 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";
1
+ import { n as IMAGE_SIZES_ENV, r as IMAGE_SIZES_MARK, t as DIST_DIR_RECORD } from "./image-sizes-mark-BOE7ObB9.js";
2
+ import { CALENDARS_DIR, FEEDS_DIR, TRAILING_SLASH_VARIABLE, allPages, brokerOrigin, buildNotes, configAdmin, configBroker, loadSiteContent, normalizeBase, redirectFrom, redirectProblems, shownContent, vercelAdminRedirects, vercelHeaders } from "@goodfellow-cms/core";
3
+ import { BUILD_DATES_DIR, BUILD_DATES_ENV, DEV_API_PREFIX, builtSiteSource, contentRepositoryProblem, handleDevApi, hostFonts, isAllowedDevHost, isLocalOrigin, localFileStore, localRegistryDir } from "@goodfellow-cms/core/node";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { mkdir, rm, writeFile } from "node:fs/promises";
6
6
  import { createServer } from "node:http";
@@ -71,6 +71,11 @@ function startDevApi(root) {
71
71
  server: "next-dev"
72
72
  };
73
73
  const server = createServer(async (req, res) => {
74
+ const forwarded = req.headers["x-forwarded-host"];
75
+ if (!isAllowedDevHost(req.headers.host) || forwarded !== void 0 && !isAllowedDevHost(String(forwarded))) {
76
+ res.writeHead(403).end();
77
+ return;
78
+ }
74
79
  if (await (await editor).serve(req, res)) return;
75
80
  if (!await handleDevApi(store, req, res, {
76
81
  trustOrigin: isLocalOrigin,
@@ -114,6 +119,25 @@ function redirectSource(path) {
114
119
  return encodeURI(path).replace(/[()[\]{}+*?:!]/g, "\\$&");
115
120
  }
116
121
  /**
122
+ * A redirect's target as Next.js's destination: Next.js reads `:name`, `*`,
123
+ * `+`, `(`, `)`, `{` and `}` in its path and hash as a pattern, which would
124
+ * fail the build (`/a/:b` names a value the source doesn't have), so they're
125
+ * percent-encoded there, which browsers and servers read as the same address.
126
+ * Next.js escapes its query values itself.
127
+ */
128
+ function redirectDestination(to) {
129
+ const target = to.trim();
130
+ const origin = /^https:\/\/[^/?#]*/i.exec(target)?.[0] ?? "";
131
+ const rest = target.slice(origin.length);
132
+ const query = rest.indexOf("?");
133
+ const hash = rest.indexOf("#");
134
+ const encode = (part) => part.replace(/[:*+(){}]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
135
+ const pathEnd = [query, hash].filter((index) => index >= 0).reduce((a, b) => Math.min(a, b), rest.length);
136
+ const search = query >= 0 && (hash < 0 || query < hash) ? rest.slice(query, hash > query ? hash : void 0) : "";
137
+ const fragment = hash >= 0 ? rest.slice(hash) : "";
138
+ return `${origin}${encode(rest.slice(0, pathEnd))}${search}${encode(fragment)}`;
139
+ }
140
+ /**
117
141
  * The site's redirects (`content/redirects.json`) for hosts that send them
118
142
  * themselves, such as Vercel, which reads them from the build. Redirects from
119
143
  * an address a page or a collection's item has are left out, since pages win,
@@ -130,7 +154,7 @@ async function siteRedirects(root) {
130
154
  if (problems[index] || !from.ok || taken.has(from.path)) return [];
131
155
  return [{
132
156
  source: redirectSource(from.path),
133
- destination: redirect.to.trim(),
157
+ destination: redirectDestination(redirect.to),
134
158
  permanent: true
135
159
  }];
136
160
  });
@@ -193,10 +217,13 @@ function withGoodfellow(nextConfig = {}, options = {}) {
193
217
  images: imageSettings(nextConfig.images),
194
218
  env: {
195
219
  ...nextConfig.env,
196
- GOODFELLOW_BASE_PATH: basePath
220
+ GOODFELLOW_BASE_PATH: basePath,
221
+ [TRAILING_SLASH_VARIABLE]: String(nextConfig.trailingSlash ?? true)
197
222
  }
198
223
  };
199
224
  if (phase === PHASE_PRODUCTION_BUILD) {
225
+ await mkdir(dirname(join(root, DIST_DIR_RECORD)), { recursive: true });
226
+ await writeFile(join(root, DIST_DIR_RECORD), nextConfig.distDir ?? ".next");
200
227
  const dates = join(root, nextConfig.distDir ?? ".next", BUILD_DATES_DIR);
201
228
  await rm(dates, {
202
229
  recursive: true,
@@ -276,4 +303,4 @@ function withGoodfellow(nextConfig = {}, options = {}) {
276
303
  };
277
304
  }
278
305
  //#endregion
279
- export { withGoodfellow };
306
+ export { redirectDestination, withGoodfellow };
package/dist/export.d.ts CHANGED
@@ -2,11 +2,6 @@ import { GoodfellowConfig, PluginTaskRequest, SelfHostedResult, SiteProblem, Tas
2
2
  import { ImageSizesResult, SiteFile } from "@goodfellow-cms/core/node";
3
3
  import { PageRender } from "@goodfellow-cms/core/shadcn";
4
4
  //#region src/site-files.d.ts
5
- /**
6
- * The Next.js starter's files that `goodfellow-next update` keeps up to date
7
- * where nobody has changed them, as the starter now has them. A test checks
8
- * they match `templates/next`.
9
- */
10
5
  export declare const NEXT_SITE_FILES: readonly SiteFile[];
11
6
  //#endregion
12
7
  //#region src/export.d.ts
@@ -35,6 +30,15 @@ export declare function writeContentApi(outDir: string, root?: string): Promise<
35
30
  * code, such as `es/feeds/`.
36
31
  */
37
32
  export declare function buildFileFolders(root?: string): Promise<string[]>;
33
+ /** The folder `next build` wrote into, as `withGoodfellow()` recorded it: the config's `distDir`, or `.next`. */
34
+ export declare function nextDistDir(root?: string): Promise<string>;
35
+ /**
36
+ * Whether the export's pages end their addresses with `/`, as Next.js's
37
+ * `trailingSlash` says, which Next.js records in
38
+ * `.next/required-server-files.json`. `true` where it can't be read, as
39
+ * `withGoodfellow()` sets it.
40
+ */
41
+ export declare function exportTrailingSlash(root?: string): Promise<boolean>;
38
42
  /** What `writePluginFiles()` wrote and ran. */
39
43
  export interface PluginFilesWritten {
40
44
  /** The plugins whose admin frames were written. */
@@ -156,10 +160,14 @@ export declare function exportedPages(outDir: string): Promise<BuiltPage[]>;
156
160
  */
157
161
  export declare function checkPages(pages: readonly BuiltPage[], root?: string): Promise<SiteProblem[]>;
158
162
  /**
159
- * Runs one of a plugin's tasks for the admin panel, as `goodfellow task`
160
- * does for other sites, with the site's config loaded in Node, and writes its
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.
163
+ * Runs one of a plugin's tasks for the admin panel in one job, as
164
+ * `goodfellow task` does for other sites, with the site's config loaded in
165
+ * Node, and writes its answer sealed to the request's key, even when the task
166
+ * fails: for sites' task setups from before 0.9. The config loads in a process
167
+ * of its own, without the job's secrets in its environment, but where it can
168
+ * read its parent's, so the starters now write the task down with
169
+ * `goodfellow-next task --describe` in a job with no secrets and run it with
170
+ * `@goodfellow-cms/core`'s `goodfellow-task`.
163
171
  */
164
172
  export declare function writeTaskResult(root: string, request: PluginTaskRequest, out?: string): Promise<TaskResultFile>;
165
173
  /** The site's config, loaded in Node, for `writeTaskResult()`'s process that loads it apart from the job's secrets. */
package/dist/export.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as loadSiteModule, i as loadSiteConfig, n as buildPluginFrames, r as configHasPlugins } from "./plugins-UKkfu0Uj.js";
2
- import "./image-sizes-mark-Ctz9p44g.js";
3
- import { r as NEXT_SITE_FILES } from "./site-files-BeLnLxCl.js";
2
+ import { t as DIST_DIR_RECORD } from "./image-sizes-mark-BOE7ObB9.js";
3
+ import { r as NEXT_SITE_FILES } from "./site-files-Cmu3ovIs.js";
4
4
  import { CALENDARS_DIR, CLOUDFLARE_REDIRECTS_PATH, EDITOR_PRELUDE, FEEDS_DIR, SITE_DESCRIPTION_PATH, TASK_RESULT_FILE, absoluteUrl, allPages, calendarFiles, cloudflareRedirects, editorPolicy, feedFiles, hasLanguages, loadSiteContent, parseSiteDescription, pathLanguage, pluginId, redirectFiles, rootRobotsUrl, sealTaskAnswer, shownContent, siteLanguages, sitePages, taskRequestProblem, todayIn } from "@goodfellow-cms/core";
5
5
  import { BUILD_DATES_DIR, builtSiteSource, guardBuiltSite, reportedDate, runPluginBuildSteps, runPluginTask, selfHostFonts, sitePluginsApart, taskSecretsFrom, writeBuildRecord, writeImageSizes, writeSiteHeaders } from "@goodfellow-cms/core/node";
6
6
  import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
@@ -71,16 +71,34 @@ async function buildFileFolders(root = process.cwd()) {
71
71
  const { settings } = await loadSiteContent(builtSiteSource(resolve(root)));
72
72
  return siteLanguages(settings).flatMap((language) => [CALENDARS_DIR, FEEDS_DIR].map((dir) => `${language.prefix.slice(1)}${language.prefix ? "/" : ""}${dir}`));
73
73
  }
74
+ /** The folder `next build` wrote into, as `withGoodfellow()` recorded it: the config's `distDir`, or `.next`. */
75
+ async function nextDistDir(root = process.cwd()) {
76
+ const recorded = await readFile(join(root, DIST_DIR_RECORD), "utf8").catch(() => "");
77
+ return join(root, recorded.trim() || ".next");
78
+ }
74
79
  /** The `basePath` the export was built with, which Next.js records in `.next/routes-manifest.json`. */
75
80
  async function exportBasePath(root) {
76
81
  try {
77
- const manifest = JSON.parse(await readFile(join(root, ".next/routes-manifest.json"), "utf8"));
82
+ const manifest = JSON.parse(await readFile(join(await nextDistDir(root), "routes-manifest.json"), "utf8"));
78
83
  return typeof manifest.basePath === "string" ? manifest.basePath : "";
79
84
  } catch {
80
85
  return "";
81
86
  }
82
87
  }
83
88
  /**
89
+ * Whether the export's pages end their addresses with `/`, as Next.js's
90
+ * `trailingSlash` says, which Next.js records in
91
+ * `.next/required-server-files.json`. `true` where it can't be read, as
92
+ * `withGoodfellow()` sets it.
93
+ */
94
+ async function exportTrailingSlash(root = ".") {
95
+ try {
96
+ return JSON.parse(await readFile(join(await nextDistDir(root), "required-server-files.json"), "utf8")).config?.trailingSlash !== false;
97
+ } catch {
98
+ return true;
99
+ }
100
+ }
101
+ /**
84
102
  * Plugins' parts that builds make: each plugin's admin frame, beside the
85
103
  * editor's frames at `admin/plugins/<id>/`, and its build steps, each in a
86
104
  * process of its own held to what the plugin declared. Into a static export
@@ -138,7 +156,7 @@ async function writeFontFiles(outDir, root = process.cwd()) {
138
156
  async function writeBuildRecordFile(outDir, root = process.cwd()) {
139
157
  const dir = resolve(root);
140
158
  const content = await loadSiteContent(builtSiteSource(dir));
141
- const date = await reportedDate(join(dir, ".next", BUILD_DATES_DIR));
159
+ const date = await reportedDate(join(await nextDistDir(dir), BUILD_DATES_DIR));
142
160
  return (await writeBuildRecord(outDir, {
143
161
  root: dir,
144
162
  content,
@@ -169,7 +187,7 @@ async function writeRedirectFiles(outDir, root = process.cwd()) {
169
187
  await writeFile(target, file.content);
170
188
  written.push(file);
171
189
  }
172
- const cloudflare = cloudflareRedirects(written, base);
190
+ const cloudflare = cloudflareRedirects(written, base, content.settings.trailingSlash !== false);
173
191
  const redirects = join(outDir, CLOUDFLARE_REDIRECTS_PATH.slice(1));
174
192
  if (cloudflare.file && !await exists(redirects)) await writeFile(redirects, cloudflare.file.content);
175
193
  return {
@@ -185,14 +203,14 @@ async function writeRedirectFiles(outDir, root = process.cwd()) {
185
203
  */
186
204
  async function writeImageFiles(outDir, root = process.cwd()) {
187
205
  const dir = resolve(root);
188
- if (!await exists(join(dir, ".next", "goodfellow-image-sizes"))) return void 0;
206
+ if (!await exists(join(await nextDistDir(dir), "goodfellow-image-sizes"))) return void 0;
189
207
  return writeImageSizes(dir, outDir, { resize: await sharpResizer() });
190
208
  }
191
- /** Makes the copies with sharp, turned as each photo's details say and without them, unless it can't load here. */
209
+ /** Makes the copies with sharp, as WebP, turned as each photo's details say and without them, unless it can't load here. */
192
210
  async function sharpResizer() {
193
211
  try {
194
212
  const { default: sharp } = await import("sharp");
195
- return async (image, width) => new Uint8Array(await sharp(image).rotate().resize({ width }).toBuffer());
213
+ return async (image, width) => new Uint8Array(await sharp(image).rotate().resize({ width }).webp({ quality: 80 }).toBuffer());
196
214
  } catch {
197
215
  return;
198
216
  }
@@ -311,7 +329,7 @@ async function writePageLanguages(outDir, root = process.cwd()) {
311
329
  const dir = resolve(root);
312
330
  const { settings } = await loadSiteContent(builtSiteSource(dir));
313
331
  if (!hasLanguages(settings)) return 0;
314
- const base = outDir ?? join(dir, ".next/server/app");
332
+ const base = outDir ?? join(await nextDistDir(dir), "server/app");
315
333
  const files = (await readdir(base, { recursive: true })).map((file) => file.split("\\").join("/")).filter((file) => file.endsWith(".html") && !/^(admin|_next|pagefind)\//.test(file));
316
334
  let changed = 0;
317
335
  for (const file of files) {
@@ -331,7 +349,7 @@ async function writePageLanguages(outDir, root = process.cwd()) {
331
349
  * and keeps their HTML in `.next/server/app/`. The admin panel and Next.js's own pages are left out.
332
350
  */
333
351
  async function builtPages(root = process.cwd()) {
334
- const next = join(resolve(root), ".next");
352
+ const next = await nextDistDir(resolve(root));
335
353
  const manifest = JSON.parse(await readFile(join(next, "prerender-manifest.json"), "utf8"));
336
354
  const trailingSlash = JSON.parse(await readFile(join(next, "required-server-files.json"), "utf8")).config?.trailingSlash === true;
337
355
  const pages = [];
@@ -389,10 +407,14 @@ async function checkPages(pages, root = process.cwd()) {
389
407
  }));
390
408
  }
391
409
  /**
392
- * Runs one of a plugin's tasks for the admin panel, as `goodfellow task`
393
- * does for other sites, with the site's config loaded in Node, and writes its
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.
410
+ * Runs one of a plugin's tasks for the admin panel in one job, as
411
+ * `goodfellow task` does for other sites, with the site's config loaded in
412
+ * Node, and writes its answer sealed to the request's key, even when the task
413
+ * fails: for sites' task setups from before 0.9. The config loads in a process
414
+ * of its own, without the job's secrets in its environment, but where it can
415
+ * read its parent's, so the starters now write the task down with
416
+ * `goodfellow-next task --describe` in a job with no secrets and run it with
417
+ * `@goodfellow-cms/core`'s `goodfellow-task`.
396
418
  */
397
419
  async function writeTaskResult(root, request, out = TASK_RESULT_FILE) {
398
420
  const secrets = taskSecretsFrom(process.env);
@@ -453,4 +475,4 @@ function blockPageRender(root) {
453
475
  };
454
476
  }
455
477
  //#endregion
456
- export { EDITOR_PAGES, NEXT_SITE_FILES, blockPageRender, buildFileFolders, builtPages, checkPages, exportedPages, fixSegmentFiles, guardExport, loadTaskConfig, writeBuildRecordFile, writeCalendarFiles, writeContentApi, writeEditorPolicy, writeExportHeaders, writeFeedFiles, writeFontFiles, writeImageFiles, writePageLanguages, writePluginFiles, writeRedirectFiles, writeTaskResult };
478
+ export { EDITOR_PAGES, NEXT_SITE_FILES, blockPageRender, buildFileFolders, builtPages, checkPages, exportTrailingSlash, exportedPages, fixSegmentFiles, guardExport, loadTaskConfig, nextDistDir, writeBuildRecordFile, writeCalendarFiles, writeContentApi, writeEditorPolicy, writeExportHeaders, writeFeedFiles, writeFontFiles, writeImageFiles, writePageLanguages, writePluginFiles, writeRedirectFiles, writeTaskResult };
@@ -7,5 +7,11 @@
7
7
  const IMAGE_SIZES_ENV = "GOODFELLOW_IMAGE_SIZES";
8
8
  /** The file in `.next/` that tells `goodfellow-next finish` the pages list them. */
9
9
  const IMAGE_SIZES_MARK = "goodfellow-image-sizes";
10
+ /**
11
+ * Where `withGoodfellow()` records the folder `next build` writes into (the
12
+ * config's `distDir`, `.next` unless it says otherwise), relative to the site,
13
+ * for `goodfellow-next finish`, which runs after it in a process of its own.
14
+ */
15
+ const DIST_DIR_RECORD = "node_modules/.goodfellow/next-dist-dir";
10
16
  //#endregion
11
- export { IMAGE_SIZES_MARK as n, IMAGE_SIZES_ENV as t };
17
+ export { IMAGE_SIZES_ENV as n, IMAGE_SIZES_MARK as r, DIST_DIR_RECORD as t };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AdminSite, GoodfellowConfig, Page, SiteContent } from "@goodfellow-cms/core";
1
+ import { AdminSite, GoodfellowConfig, ImageSize, Page, SiteContent } from "@goodfellow-cms/core";
2
2
  import { ReactNode } from "react";
3
3
  import { Metadata, MetadataRoute } from "next";
4
4
  //#region src/index.d.ts
@@ -24,8 +24,11 @@ export interface PageProps {
24
24
  * content edits show on reload in development.
25
25
  */
26
26
  export declare const loadSite: (root?: string) => Promise<SiteContent>;
27
- /** A page's title, description and link previews, as Next.js metadata. */
28
- export declare function pageMetadata(content: SiteContent, page: Page, basePath?: string): Metadata;
27
+ /**
28
+ * A page's title, description, search engine settings and link previews, as
29
+ * Next.js metadata, with the shared picture's size where `media` has it.
30
+ */
31
+ export declare function pageMetadata(content: SiteContent, page: Page, basePath?: string, media?: Readonly<Record<string, ImageSize>>): Metadata;
29
32
  /**
30
33
  * Renders the site's pages in a Next.js App Router site, as Server Components.
31
34
  * Blocks' links to the site's pages use `next/link`, and their images
@@ -75,7 +78,10 @@ export declare function goodfellowPages(config: GoodfellowConfig, options?: Good
75
78
  * edit its content without its code, such as the Claude connector.
76
79
  */
77
80
  siteDescription(): Response;
78
- /** For `app/robots.ts`: lets search engines in, and points them at the sitemap once the site's address is set. */
81
+ /**
82
+ * For `app/robots.ts`: lets search engines in, asks AI training's crawlers to stay out where Site Settings
83
+ * says so, and points them at the sitemap once the site's address is set (`robotsRules()`).
84
+ */
79
85
  robots(): Promise<MetadataRoute.Robots>;
80
86
  };
81
87
  //#endregion
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { ByLanguage, CurrentMenuLinks, NextImage, NextLink, PageLanguage, SearchResultLinks } from "./components.js";
2
- import "./image-sizes-mark-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";
1
+ import { ByLanguage, CurrentMenuLinks, GoatCounterNavigation, NextImage, NextLink, PageLanguage, SearchResultLinks } from "./components.js";
2
+ import "./image-sizes-mark-BOE7ObB9.js";
3
+ import { BLOCK_SCRIPTS, DARK_MODE_ATTRIBUTE, IMAGE_WIDTHS, TRAILING_SLASH_VARIABLE, absoluteUrl, adminSiteForCopy, colorSchemeMeta, colorSchemeScript, consentScript, contentForPage, customFontPreloads, demoContent, describedForCopy, feedLinks, findRedirect, fontPreloads, googleFontsUrl, hasLanguages, heldCode, heldStats, languageMap, loadSiteContent, normalizePagePath, pageHead, robotsRules, shownContent, siteCopyFrom, siteLanguages, sitePages, sitemapPages, storedMediaSizes, themeToCss, todayIn, withBase, withSelfHostedFonts } from "@goodfellow-cms/core";
4
4
  import { BUILD_DATES_ENV, builtSiteSource, fileSystemSource, fontStandIns, readMediaSizes, readStylesheet, reportDatesTo } from "@goodfellow-cms/core/node";
5
- import { BodyCode, HeadCode, PageBody, PageContent, PageFooter, PageHeader, adminSite, applyPageEntry, createPuckConfigs, describeSite, prepareLayout, preparePage } from "@goodfellow-cms/react";
5
+ import { BodyCode, HeadCode, PageBody, PageContent, PageFooter, PageHeader, StatsCode, adminSite, applyPageEntry, createPuckConfigs, describeSite, prepareLayout, preparePage } from "@goodfellow-cms/react";
6
6
  import { notFound, redirect } from "next/navigation";
7
7
  import { cache } from "react";
8
8
  import { preload } from "react-dom";
@@ -41,7 +41,10 @@ function hostedFonts(source) {
41
41
  * for the length of one request, so a page and its metadata read it once, and
42
42
  * content edits show on reload in development.
43
43
  */
44
- const loadSite = cache(async (root = process.cwd()) => shownContent(await loadSiteContent(builtSiteSource(root))));
44
+ const loadSite = cache(async (root = process.cwd()) => shownContent(await loadSiteContent(builtSiteSource(root, {
45
+ ...process.env,
46
+ [TRAILING_SLASH_VARIABLE]: process.env.GOODFELLOW_TRAILING_SLASH
47
+ }))));
45
48
  function pathOf(segments) {
46
49
  try {
47
50
  return normalizePagePath(`/${(segments ?? []).map((segment) => decodeURIComponent(segment)).join("/")}`);
@@ -80,16 +83,25 @@ function toAbsolute(siteUrl, url) {
80
83
  if (!url || !siteUrl || !url.startsWith("/") || url.startsWith("//")) return url;
81
84
  return absoluteUrl(siteUrl, url);
82
85
  }
83
- /** A page's title, description and link previews, as Next.js metadata. */
84
- function pageMetadata(content, page, basePath = configuredBasePath()) {
86
+ /**
87
+ * A page's title, description, search engine settings and link previews, as
88
+ * Next.js metadata, with the shared picture's size where `media` has it.
89
+ */
90
+ function pageMetadata(content, page, basePath = configuredBasePath(), media = {}) {
85
91
  const head = pageHead(content, page);
86
92
  const socialImage = toAbsolute(content.settings.url, head.socialImage);
93
+ const socialSize = head.socialImage ? media[head.socialImage] : void 0;
87
94
  const feeds = feedLinks(contentForPage(content, page));
88
95
  return {
89
96
  ...content.settings.url && { metadataBase: new URL(content.settings.url) },
90
97
  title: { absolute: head.title },
91
98
  ...head.description && { description: head.description },
92
- ...head.noIndex && { robots: { index: false } },
99
+ robots: {
100
+ ...head.noIndex && { index: false },
101
+ ...head.noFollow && { follow: false },
102
+ ...!head.noIndex && head.noSnippet && { nosnippet: true },
103
+ ...!head.noIndex && { "max-image-preview": "large" }
104
+ },
93
105
  ...(head.canonicalUrl || feeds.length > 0) && { alternates: {
94
106
  ...head.canonicalUrl && { canonical: head.canonicalUrl },
95
107
  ...head.alternates && { languages: Object.fromEntries(head.alternates.map(({ language, href }) => [language, href])) },
@@ -100,12 +112,25 @@ function pageMetadata(content, page, basePath = configuredBasePath()) {
100
112
  } },
101
113
  ...head.favicon && { icons: { icon: withBase(head.favicon, basePath) } },
102
114
  openGraph: {
103
- type: "website",
115
+ ...head.type === "article" ? {
116
+ type: "article",
117
+ ...head.publishedTime && { publishedTime: head.publishedTime }
118
+ } : { type: "website" },
119
+ siteName: head.siteName,
120
+ locale: head.locale,
104
121
  title: head.title,
105
122
  ...head.description && { description: head.description },
106
123
  ...head.canonicalUrl && { url: head.canonicalUrl },
107
- ...socialImage && /^https?:\/\//.test(socialImage) && { images: [socialImage] }
108
- }
124
+ ...socialImage && /^https?:\/\//.test(socialImage) && { images: [{
125
+ url: socialImage,
126
+ ...socialSize && {
127
+ width: socialSize.width,
128
+ height: socialSize.height
129
+ },
130
+ ...head.socialImageAlt && { alt: head.socialImageAlt }
131
+ }] }
132
+ },
133
+ twitter: { card: head.card }
109
134
  };
110
135
  }
111
136
  /**
@@ -135,7 +160,12 @@ function goodfellowPages(config, options = {}) {
135
160
  const googleUrl = googleFontsUrl(content.settings.theme);
136
161
  const hosted = googleUrl ? hostedFonts(googleUrl) : void 0;
137
162
  const fontsUrl = googleUrl && hosted ? withSelfHostedFonts(googleUrl, hosted, basePath()) : googleUrl;
138
- const [fontFiles, standIns] = await Promise.all([fontPreloads(content.settings.theme, readStylesheet), fontStandIns(content.settings.theme)]);
163
+ const [fontFiles, standIns] = await Promise.all([fontPreloads(content.settings.theme, readStylesheet), fontStandIns(content.settings.theme, { root })]);
164
+ for (const file of customFontPreloads(content.settings.theme)) preload(withBase(file, basePath()), {
165
+ as: "font",
166
+ type: "font/woff2",
167
+ crossOrigin: ""
168
+ });
139
169
  for (const file of fontFiles) {
140
170
  const href = hosted ? withSelfHostedFonts(file, hosted, basePath()) : file;
141
171
  if (hosted && href === file) continue;
@@ -156,7 +186,7 @@ function goodfellowPages(config, options = {}) {
156
186
  /* @__PURE__ */ jsx("style", {
157
187
  href: "goodfellow-theme",
158
188
  precedence: "default",
159
- children: themeToCss(content.settings.theme, standIns)
189
+ children: themeToCss(content.settings.theme, standIns, { base: basePath() })
160
190
  }),
161
191
  fontsUrl && /* @__PURE__ */ jsx("link", {
162
192
  rel: "stylesheet",
@@ -173,7 +203,8 @@ function goodfellowPages(config, options = {}) {
173
203
  async generateMetadata({ params }) {
174
204
  const content = await site();
175
205
  const page = findPage(content, config.blocks, pathOf((await params).path));
176
- return page ? pageMetadata(content, applyPageEntry(configs, content, page), basePath()) : {};
206
+ if (!page) return {};
207
+ return pageMetadata(content, applyPageEntry(configs, content, page), basePath(), (await renderOptions()).media);
177
208
  },
178
209
  /**
179
210
  * For `app/admin/page.tsx`: what the admin panel's page knows about the
@@ -246,10 +277,15 @@ function goodfellowPages(config, options = {}) {
246
277
  held: heldCode(content.settings.consent, "head"),
247
278
  siteUrl: content.settings.url
248
279
  }),
280
+ /* @__PURE__ */ jsx(StatsCode, {
281
+ stats: content.settings.stats,
282
+ held: heldStats(content.settings.consent, content.settings.stats)
283
+ }),
249
284
  header,
250
285
  children,
251
286
  footer,
252
287
  /* @__PURE__ */ jsx(CurrentMenuLinks, { basePath: basePath() }),
288
+ content.settings.stats?.service === "goatcounter" && process.env.NODE_ENV !== "development" && /* @__PURE__ */ jsx(GoatCounterNavigation, {}),
253
289
  /* @__PURE__ */ jsx(SearchResultLinks, { basePath: basePath() }),
254
290
  /* @__PURE__ */ jsx(BodyCode, {
255
291
  code: content.code?.body,
@@ -337,15 +373,15 @@ function goodfellowPages(config, options = {}) {
337
373
  siteDescription() {
338
374
  return Response.json(describedForCopy(describeSite(config), siteCopyFrom(process.env)));
339
375
  },
340
- /** For `app/robots.ts`: lets search engines in, and points them at the sitemap once the site's address is set. */
376
+ /**
377
+ * For `app/robots.ts`: lets search engines in, asks AI training's crawlers to stay out where Site Settings
378
+ * says so, and points them at the sitemap once the site's address is set (`robotsRules()`).
379
+ */
341
380
  async robots() {
342
- const url = (await site()).settings.url;
381
+ const { settings } = await site();
343
382
  return {
344
- rules: {
345
- userAgent: "*",
346
- allow: "/"
347
- },
348
- ...url && { sitemap: absoluteUrl(url, "/sitemap.xml") }
383
+ rules: robotsRules(settings),
384
+ ...settings.url && { sitemap: absoluteUrl(settings.url, "/sitemap.xml") }
349
385
  };
350
386
  }
351
387
  };
@@ -1,6 +1,8 @@
1
1
  //#region src/site-files.ts
2
2
  /** The release whose Next.js sites have the editor's sandboxed frames at `app/admin/editor/`. */
3
3
  const FRAMES_RELEASE = "0.5.0";
4
+ /** The release whose Next.js sites tell Vercel not to build drafts' and plugins' tasks' branches. */
5
+ const VERCEL_BRANCHES_RELEASE = "0.9.0";
4
6
  /** `app/admin/page.tsx` as every release before the frames wrote it, loading the site's config itself. */
5
7
  const CLIENT_ADMIN_PAGE = `"use client";
6
8
 
@@ -30,21 +32,33 @@ export default function EditorPage() {
30
32
  return <GoodfellowEditor config={config} />;
31
33
  }
32
34
  `;
33
- /**
34
- * The Next.js starter's files that `goodfellow-next update` keeps up to date
35
- * where nobody has changed them, as the starter now has them. A test checks
36
- * they match `templates/next`.
37
- */
38
- const NEXT_SITE_FILES = [{
39
- path: "app/admin/page.tsx",
40
- since: FRAMES_RELEASE,
41
- content: ADMIN_PAGE,
42
- earlier: [CLIENT_ADMIN_PAGE]
43
- }, {
44
- path: "app/admin/editor/page.tsx",
45
- since: FRAMES_RELEASE,
46
- content: EDITOR_PAGE,
47
- earlier: []
48
- }];
35
+ const NEXT_SITE_FILES = [
36
+ {
37
+ path: "app/admin/page.tsx",
38
+ since: FRAMES_RELEASE,
39
+ content: ADMIN_PAGE,
40
+ earlier: [CLIENT_ADMIN_PAGE]
41
+ },
42
+ {
43
+ path: "app/admin/editor/page.tsx",
44
+ since: FRAMES_RELEASE,
45
+ content: EDITOR_PAGE,
46
+ earlier: []
47
+ },
48
+ {
49
+ path: "vercel.json",
50
+ since: VERCEL_BRANCHES_RELEASE,
51
+ content: `{
52
+ "git": {
53
+ "deploymentEnabled": {
54
+ "goodfellow-draft-*": false,
55
+ "goodfellow-task-*": false
56
+ }
57
+ }
58
+ }
59
+ `,
60
+ earlier: []
61
+ }
62
+ ];
49
63
  //#endregion
50
64
  export { EDITOR_PAGE as n, NEXT_SITE_FILES as r, ADMIN_PAGE as t };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goodfellow-cms/next",
3
- "version": "0.8.0",
4
- "description": "Goodfellow pages and the admin panel in a Next.js site.",
3
+ "version": "0.9.0",
4
+ "description": "Goodfellow CMS pages and the admin panel in a Next.js site.",
5
5
  "keywords": [
6
6
  "goodfellow",
7
7
  "cms",
@@ -12,6 +12,10 @@
12
12
  ],
13
13
  "homepage": "https://goodfellow-cms.github.io/goodfellow-cms/docs/nextjs",
14
14
  "bugs": "https://github.com/goodfellow-cms/goodfellow-cms/issues",
15
+ "funding": {
16
+ "type": "github",
17
+ "url": "https://github.com/sponsors/goodfellow-cms"
18
+ },
15
19
  "license": "MIT",
16
20
  "repository": {
17
21
  "type": "git",
@@ -63,24 +67,24 @@
63
67
  "dist"
64
68
  ],
65
69
  "dependencies": {
66
- "@goodfellow-cms/ai": "0.8.0",
67
- "@goodfellow-cms/core": "0.8.0",
68
- "@goodfellow-cms/react": "0.8.0",
70
+ "@goodfellow-cms/ai": "0.9.0",
71
+ "@goodfellow-cms/core": "0.9.0",
72
+ "@goodfellow-cms/react": "0.9.0",
69
73
  "rolldown": "1.2.12",
70
74
  "sharp": "0.35.5"
71
75
  },
72
76
  "peerDependencies": {
73
- "@goodfellow-cms/admin": "0.8.0",
77
+ "@goodfellow-cms/admin": "0.9.0",
74
78
  "@puckeditor/core": "0.23.0",
75
79
  "next": "^16.0.0",
76
80
  "react": "^19.0.0",
77
81
  "react-dom": "^19.0.0"
78
82
  },
79
83
  "devDependencies": {
80
- "@goodfellow-cms/admin": "0.8.0",
81
- "@goodfellow-cms/bitbucket": "0.8.0",
82
- "@goodfellow-cms/github": "0.8.0",
83
- "@goodfellow-cms/gitlab": "0.8.0",
84
+ "@goodfellow-cms/admin": "0.9.0",
85
+ "@goodfellow-cms/bitbucket": "0.9.0",
86
+ "@goodfellow-cms/github": "0.9.0",
87
+ "@goodfellow-cms/gitlab": "0.9.0",
84
88
  "@puckeditor/core": "0.23.0",
85
89
  "@tailwindcss/browser": "4.3.3",
86
90
  "@types/react": "^19.2.0",