@goodfellow-cms/next 0.0.0-stage → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,200 @@
1
+ import { CurrentMenuLinks, NextImage, NextLink } from "./components.js";
2
+ import { absoluteUrl, allPages, demoContent, getPageHead, googleFontsUrl, loadSiteContent, normalizePagePath, themeToCss, withBase } from "@goodfellow-cms/core";
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";
6
+ import { cache } from "react";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/index.tsx
9
+ /** Next.js's own link and image components, for blocks' `<SiteLink>` and `<SiteImage>`. */
10
+ const components = {
11
+ Link: NextLink,
12
+ Image: NextImage
13
+ };
14
+ /** Set by `withGoodfellow()` from Next.js's `basePath`. */
15
+ function configuredBasePath() {
16
+ return process.env.GOODFELLOW_BASE_PATH ?? "";
17
+ }
18
+ /**
19
+ * Reads the site's content from disk. Cached for the length of one request, so
20
+ * a page and its metadata read it once, and content edits show on reload in development.
21
+ */
22
+ const loadSite = cache((root = process.cwd()) => loadSiteContent(fileSystemSource(root)));
23
+ function pathOf(segments) {
24
+ try {
25
+ return normalizePagePath(`/${(segments ?? []).map((segment) => decodeURIComponent(segment)).join("/")}`);
26
+ } catch {
27
+ return "";
28
+ }
29
+ }
30
+ function findPage(content, path) {
31
+ return allPages(content).find((page) => page.path === path);
32
+ }
33
+ function toAbsolute(siteUrl, url) {
34
+ if (!url || !siteUrl || !url.startsWith("/") || url.startsWith("//")) return url;
35
+ return absoluteUrl(siteUrl, url);
36
+ }
37
+ /** A page's title, description and link previews, as Next.js metadata. */
38
+ function pageMetadata(content, page, basePath = configuredBasePath()) {
39
+ const head = getPageHead(content.settings, page);
40
+ const socialImage = toAbsolute(content.settings.url, head.socialImage);
41
+ return {
42
+ ...content.settings.url && { metadataBase: new URL(content.settings.url) },
43
+ title: { absolute: head.title },
44
+ ...head.description && { description: head.description },
45
+ ...head.noIndex && { robots: { index: false } },
46
+ ...head.canonicalUrl && { alternates: { canonical: head.canonicalUrl } },
47
+ ...head.favicon && { icons: { icon: withBase(head.favicon, basePath) } },
48
+ openGraph: {
49
+ type: "website",
50
+ title: head.title,
51
+ ...head.description && { description: head.description },
52
+ ...head.canonicalUrl && { url: head.canonicalUrl },
53
+ ...socialImage && /^https?:\/\//.test(socialImage) && { images: [socialImage] }
54
+ }
55
+ };
56
+ }
57
+ /**
58
+ * Renders the site's pages in a Next.js App Router site, as Server Components.
59
+ * Blocks' links to the site's pages use `next/link`, and their images
60
+ * `next/image`, through `<SiteLink>` and `<SiteImage>`; Client Components in
61
+ * blocks run in the browser. Use the results in `app/(site)/layout.tsx`,
62
+ * which shows the header and footer once for every page, in
63
+ * `app/(site)/[[...path]]/page.tsx`, and in `app/not-found.tsx`.
64
+ */
65
+ function goodfellowPages(config, options = {}) {
66
+ const configs = createPuckConfigs(config);
67
+ const root = options.root ?? process.cwd();
68
+ const site = () => loadSite(options.root);
69
+ const basePath = () => options.basePath ?? configuredBasePath();
70
+ const renderOptions = async () => ({
71
+ base: basePath(),
72
+ components,
73
+ media: await readMediaSizes(root)
74
+ });
75
+ /** The site's theme and fonts, which React moves into the document's head. themeToCss escapes its output. */
76
+ function Theme({ content }) {
77
+ const fontsUrl = googleFontsUrl(content.settings.theme);
78
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("style", {
79
+ href: "goodfellow-theme",
80
+ precedence: "default",
81
+ children: themeToCss(content.settings.theme)
82
+ }), fontsUrl && /* @__PURE__ */ jsxs(Fragment, { children: [
83
+ /* @__PURE__ */ jsx("link", {
84
+ rel: "preconnect",
85
+ href: "https://fonts.googleapis.com"
86
+ }),
87
+ /* @__PURE__ */ jsx("link", {
88
+ rel: "preconnect",
89
+ href: "https://fonts.gstatic.com",
90
+ crossOrigin: ""
91
+ }),
92
+ /* @__PURE__ */ jsx("link", {
93
+ rel: "stylesheet",
94
+ href: fontsUrl,
95
+ precedence: "default"
96
+ })
97
+ ] })] });
98
+ }
99
+ return {
100
+ /** Every page's address, so Next.js builds them all. Any other address is "not found". */
101
+ async generateStaticParams() {
102
+ return allPages(await site()).filter((page) => page.path !== "/404").map((page) => ({ path: page.path === "/" ? [] : page.path.slice(1).split("/") }));
103
+ },
104
+ async generateMetadata({ params }) {
105
+ const content = await site();
106
+ const page = findPage(content, pathOf((await params).path));
107
+ return page ? pageMetadata(content, applyPageEntry(configs, content, page), basePath()) : {};
108
+ },
109
+ /**
110
+ * For `app/(site)/layout.tsx`: the site's header and footer around every page, kept as
111
+ * they are when moving between pages. Menus mark the current page in the browser.
112
+ */
113
+ async Layout({ children }) {
114
+ const content = await site();
115
+ const layout = await prepareLayout(configs, content, await renderOptions());
116
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
117
+ /* @__PURE__ */ jsx(Theme, { content }),
118
+ /* @__PURE__ */ jsx(HeadCode, { code: content.code?.head }),
119
+ /* @__PURE__ */ jsx(PageHeader, {
120
+ site: layout.site,
121
+ layoutConfig: layout.layoutConfig,
122
+ data: layout.header
123
+ }),
124
+ children,
125
+ /* @__PURE__ */ jsx(PageFooter, {
126
+ site: layout.site,
127
+ layoutConfig: layout.layoutConfig,
128
+ data: layout.footer
129
+ }),
130
+ /* @__PURE__ */ jsx(CurrentMenuLinks, { basePath: basePath() }),
131
+ /* @__PURE__ */ jsx(BodyCode, { code: content.code?.body })
132
+ ] });
133
+ },
134
+ /** The page at the route's address, inside `Layout`'s header and footer. */
135
+ async Page({ params }) {
136
+ const content = await site();
137
+ const page = findPage(content, pathOf((await params).path));
138
+ if (!page || page.path === "/404") notFound();
139
+ const prepared = await preparePage(configs, content, page, await renderOptions());
140
+ return /* @__PURE__ */ jsx(PageContent, {
141
+ site: prepared.site,
142
+ pageConfig: prepared.pageConfig,
143
+ page: prepared.data
144
+ });
145
+ },
146
+ /**
147
+ * The site's "Page not found" page (`content/pages/404.json`), for `app/not-found.tsx`,
148
+ * with the header and footer, since Next.js shows it outside the `(site)` layout.
149
+ */
150
+ async NotFound() {
151
+ const content = await site();
152
+ const page = findPage(content, "/404");
153
+ if (!page) return /* @__PURE__ */ jsx("h1", { children: "Page not found" });
154
+ const prepared = await preparePage(configs, content, page, await renderOptions());
155
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
156
+ /* @__PURE__ */ jsx("title", { children: getPageHead(content.settings, page).title }),
157
+ /* @__PURE__ */ jsx(Theme, { content }),
158
+ /* @__PURE__ */ jsx(HeadCode, { code: content.code?.head }),
159
+ /* @__PURE__ */ jsx(PageBody, {
160
+ site: prepared.site,
161
+ pageConfig: prepared.pageConfig,
162
+ layoutConfig: prepared.layoutConfig,
163
+ page: prepared.data,
164
+ header: prepared.header,
165
+ footer: prepared.footer
166
+ }),
167
+ /* @__PURE__ */ jsx(BodyCode, { code: content.code?.body })
168
+ ] });
169
+ },
170
+ /** For `app/sitemap.ts`: every page but "Page not found", once the site's address is set. */
171
+ async sitemap() {
172
+ const content = await site();
173
+ const url = content.settings.url;
174
+ if (!url) return [];
175
+ return allPages(content).filter((page) => page.path !== "/404").map((page) => ({ url: absoluteUrl(url, page.path) }));
176
+ },
177
+ /**
178
+ * For `app/admin/demo-content.json/route.ts`: in a demo (the config's `demo`), the copy of
179
+ * the site's content its admin panel starts from. A static export can't leave a route out,
180
+ * so other sites get an empty file.
181
+ */
182
+ async demoContent() {
183
+ if (!config.demo) return new Response(null, { status: 404 });
184
+ return new Response(await demoContent(fileSystemSource(root)), { headers: { "content-type": "application/json" } });
185
+ },
186
+ /** For `app/robots.ts`: lets search engines in, and points them at the sitemap once the site's address is set. */
187
+ async robots() {
188
+ const url = (await site()).settings.url;
189
+ return {
190
+ rules: {
191
+ userAgent: "*",
192
+ allow: "/"
193
+ },
194
+ ...url && { sitemap: absoluteUrl(url, "/sitemap.xml") }
195
+ };
196
+ }
197
+ };
198
+ }
199
+ //#endregion
200
+ export { goodfellowPages, loadSite, pageMetadata };