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