@codepress/blog 0.2.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.
@@ -0,0 +1,315 @@
1
+ import {
2
+ BLOCK_ID_ATTRIBUTE,
3
+ PROPS_ATTRIBUTE
4
+ } from "./chunk-XLJBCIXI.js";
5
+
6
+ // src/editor/config.ts
7
+ var DEFAULT_TIMINGS = {
8
+ autosaveMs: 2e3,
9
+ draftPollMs: 1e4,
10
+ publishReloadMs: 4e3
11
+ };
12
+ function resolveTimings(timings) {
13
+ const pick = (key) => {
14
+ const value = timings?.[key];
15
+ return typeof value === "number" && value > 0 ? value : DEFAULT_TIMINGS[key];
16
+ };
17
+ return {
18
+ autosaveMs: pick("autosaveMs"),
19
+ draftPollMs: pick("draftPollMs"),
20
+ publishReloadMs: pick("publishReloadMs")
21
+ };
22
+ }
23
+ var DEFAULT_COLUMN = "mx-auto w-full max-w-[720px] px-5 sm:px-8";
24
+ var DEFAULT_APP_URL = "https://codepress.dev";
25
+ var BLOG_TOOLBAR_BOTTOM_VAR = "--blog-toolbar-bottom";
26
+ var EDITOR_SCOPE_CLASS = "cp-blog-editor";
27
+
28
+ // src/editor/EditDesignLink.tsx
29
+ import { jsx } from "react/jsx-runtime";
30
+ function designTargetOf(capabilities) {
31
+ const {
32
+ organization_slug: organizationSlug,
33
+ repository_full_name: repository
34
+ } = capabilities;
35
+ return organizationSlug && repository ? { organizationSlug, repository } : null;
36
+ }
37
+ function designUrl({ organizationSlug, repository }, slug, appUrl = "") {
38
+ const query = new URLSearchParams({
39
+ repo: repository,
40
+ path: `/blog/${slug}`
41
+ });
42
+ return `${appUrl}/dashboard/${encodeURIComponent(organizationSlug)}/design?${query.toString()}`;
43
+ }
44
+ function EditDesignLink({
45
+ href,
46
+ children = "Edit design",
47
+ ...rest
48
+ }) {
49
+ if (!href) return null;
50
+ return /* @__PURE__ */ jsx(
51
+ "a",
52
+ {
53
+ ...rest,
54
+ href,
55
+ target: "_blank",
56
+ rel: "noopener noreferrer",
57
+ "data-testid": "blog-edit-design",
58
+ children
59
+ }
60
+ );
61
+ }
62
+
63
+ // src/editor/document.ts
64
+ import { isTag, isText } from "domhandler";
65
+ import { parseDocument } from "htmlparser2";
66
+ var META_ATTRIBUTE = "data-cp-meta";
67
+ var WHITESPACE = /^\s*$/;
68
+ var ASCII_SPACES = /[ \t\n\f\r]+/g;
69
+ var ASCII_EDGE_START = /^[ \t\n\f\r]+/;
70
+ var ASCII_EDGE_END = /[ \t\n\f\r]+$/;
71
+ var ASCII_BLANK = /^[ \t\n\f\r]*$/;
72
+ function textOf(node) {
73
+ return "data" in node && typeof node.data === "string" ? node.data : "";
74
+ }
75
+ function parseMeta(script) {
76
+ try {
77
+ const value = JSON.parse(script.children.map(textOf).join(""));
78
+ if (value && typeof value === "object" && !Array.isArray(value)) {
79
+ return { meta: value };
80
+ }
81
+ } catch {
82
+ }
83
+ return { meta: {}, problem: "The metadata block is not a JSON object." };
84
+ }
85
+ function parsePostSource(source) {
86
+ const doc = parseDocument(source, {
87
+ withStartIndices: true,
88
+ withEndIndices: true
89
+ });
90
+ const result = {
91
+ prefix: "",
92
+ metaText: "",
93
+ meta: {},
94
+ blocks: [],
95
+ gaps: [],
96
+ problems: []
97
+ };
98
+ let gap = "";
99
+ let sawMeta = false;
100
+ for (const node of doc.children) {
101
+ const start = node.startIndex ?? 0;
102
+ const end = (node.endIndex ?? start) + 1;
103
+ if (isText(node)) {
104
+ const raw = source.slice(start, end);
105
+ if (!WHITESPACE.test(raw)) {
106
+ result.problems.push(
107
+ `Text outside any block: "${raw.trim().slice(0, 40)}".`
108
+ );
109
+ }
110
+ gap += raw;
111
+ continue;
112
+ }
113
+ if (!isTag(node)) {
114
+ result.problems.push("The file contains a comment or directive.");
115
+ gap += source.slice(start, end);
116
+ continue;
117
+ }
118
+ if (!sawMeta) {
119
+ sawMeta = true;
120
+ if (node.name === "script" && META_ATTRIBUTE in node.attribs) {
121
+ result.prefix = gap;
122
+ result.metaText = source.slice(start, end);
123
+ const parsed = parseMeta(node);
124
+ result.meta = parsed.meta;
125
+ if (parsed.problem) result.problems.push(parsed.problem);
126
+ gap = "";
127
+ continue;
128
+ }
129
+ result.problems.push("The file has no metadata block.");
130
+ }
131
+ result.gaps.push(gap);
132
+ gap = "";
133
+ result.blocks.push({
134
+ id: node.attribs[BLOCK_ID_ATTRIBUTE] ?? null,
135
+ tag: node.name,
136
+ text: source.slice(start, end)
137
+ });
138
+ }
139
+ if (!sawMeta) result.problems.push("The file has no metadata block.");
140
+ result.gaps.push(gap);
141
+ return result;
142
+ }
143
+ function serializeMeta(meta) {
144
+ const json = JSON.stringify(meta, null, 2).split("\n").map((line) => ` ${line}`).join("\n");
145
+ return `<script type="application/json" ${META_ATTRIBUTE}>
146
+ ${json}
147
+ </script>`;
148
+ }
149
+ function sameJson(a, b) {
150
+ return JSON.stringify(canonicalJson(a)) === JSON.stringify(canonicalJson(b));
151
+ }
152
+ function canonicalJson(value) {
153
+ if (Array.isArray(value)) return value.map(canonicalJson);
154
+ if (value && typeof value === "object") {
155
+ return Object.fromEntries(
156
+ Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => [key, canonicalJson(item)])
157
+ );
158
+ }
159
+ return value;
160
+ }
161
+ function assemblePostFile(source, meta, blocks) {
162
+ const keepMeta = source && source.metaText && sameJson(source.meta, meta);
163
+ const metaText = keepMeta ? source.metaText : serializeMeta(meta);
164
+ let out = (source?.prefix ?? "") + metaText;
165
+ let gap = "\n";
166
+ blocks.forEach((block, position) => {
167
+ const origin = block.sourceIndex ?? block.replaces ?? null;
168
+ if (source && origin != null) gap = source.gaps[origin] || "\n";
169
+ else if (source && position === 0) gap = source.gaps[0] || "\n";
170
+ const original = block.sourceIndex != null ? source?.blocks[block.sourceIndex] : void 0;
171
+ out += gap + (original ? original.text : block.html);
172
+ });
173
+ const trailing = source ? source.gaps[source.gaps.length - 1] : "\n";
174
+ return out + (trailing || "\n");
175
+ }
176
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
177
+ "p",
178
+ "h1",
179
+ "h2",
180
+ "h3",
181
+ "h4",
182
+ "h5",
183
+ "h6",
184
+ "ul",
185
+ "ol",
186
+ "li",
187
+ "blockquote",
188
+ "pre",
189
+ "hr",
190
+ "table",
191
+ "thead",
192
+ "tbody",
193
+ "tr",
194
+ "th",
195
+ "td",
196
+ "figure",
197
+ "figcaption",
198
+ "div",
199
+ "cp-callout",
200
+ "cp-figure"
201
+ ]);
202
+ var VOID_TAGS = /* @__PURE__ */ new Set(["br", "hr", "img"]);
203
+ function collapse(text) {
204
+ return text.replace(ASCII_SPACES, " ");
205
+ }
206
+ function canonicalAttributes(node) {
207
+ return Object.entries(node.attribs).filter(([name]) => name !== BLOCK_ID_ATTRIBUTE).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, value]) => {
208
+ if (name === PROPS_ATTRIBUTE) {
209
+ try {
210
+ return `${name}=${JSON.stringify(canonicalJson(JSON.parse(value)))}`;
211
+ } catch {
212
+ }
213
+ }
214
+ return `${name}=${JSON.stringify(value)}`;
215
+ }).join(" ");
216
+ }
217
+ function canonicalChildren(children, preformatted, trimEdges) {
218
+ const parts = children.map(
219
+ (child) => isTag(child) ? { tag: child.name, text: canonicalElement(child, preformatted) } : {
220
+ tag: null,
221
+ text: preformatted ? textOf(child) : collapse(textOf(child))
222
+ }
223
+ );
224
+ if (!preformatted && trimEdges) {
225
+ const first = parts[0];
226
+ const last = parts[parts.length - 1];
227
+ if (first && first.tag === null)
228
+ first.text = first.text.replace(ASCII_EDGE_START, "");
229
+ if (last && last.tag === null)
230
+ last.text = last.text.replace(ASCII_EDGE_END, "");
231
+ }
232
+ return parts.filter((part, index) => {
233
+ if (preformatted || part.tag !== null) return true;
234
+ if (part.text === "") return false;
235
+ if (!ASCII_BLANK.test(part.text)) return true;
236
+ const neighbours = [parts[index - 1], parts[index + 1]];
237
+ return !neighbours.some((n) => n?.tag != null && BLOCK_TAGS.has(n.tag));
238
+ }).map((part) => part.text).join("");
239
+ }
240
+ var PARAGRAPH_WRAPPED = /* @__PURE__ */ new Set(["li", "blockquote", "cp-callout"]);
241
+ function comparableChildren(node) {
242
+ if (!PARAGRAPH_WRAPPED.has(node.name)) return node.children;
243
+ const meaningful = node.children.filter(
244
+ (child) => !(isText(child) && ASCII_BLANK.test(child.data))
245
+ );
246
+ const only = meaningful.length === 1 ? meaningful[0] : void 0;
247
+ if (only && isTag(only) && only.name === "p" && Object.keys(only.attribs).every((name) => name === BLOCK_ID_ATTRIBUTE)) {
248
+ return only.children;
249
+ }
250
+ return node.children;
251
+ }
252
+ function canonicalElement(node, preformatted) {
253
+ const attributes = canonicalAttributes(node);
254
+ const open = `<${node.name}${attributes ? ` ${attributes}` : ""}>`;
255
+ if (VOID_TAGS.has(node.name)) return open;
256
+ const inside = preformatted || node.name === "pre";
257
+ const trimEdges = BLOCK_TAGS.has(node.name);
258
+ return `${open}${canonicalChildren(comparableChildren(node), inside, trimEdges)}</${node.name}>`;
259
+ }
260
+ function canonicalBlock(html) {
261
+ return canonicalChildren(parseDocument(html).children, false, true);
262
+ }
263
+
264
+ // src/editor/header.ts
265
+ import { useLayoutEffect, useState } from "react";
266
+ var DEFAULT_HEADER_SELECTOR = "[data-blog-header]";
267
+ function findHostHeader(selector, editorRoot) {
268
+ const matched = document.querySelector(
269
+ selector ?? DEFAULT_HEADER_SELECTOR
270
+ );
271
+ if (matched) return matched;
272
+ return Array.from(document.querySelectorAll("header")).find(
273
+ (element) => !editorRoot?.contains(element)
274
+ ) ?? null;
275
+ }
276
+ function useHeaderOffset(header, own) {
277
+ const selector = header?.selector;
278
+ const offset = header?.offset;
279
+ const [height, setHeight] = useState(0);
280
+ useLayoutEffect(() => {
281
+ if (offset !== void 0) return;
282
+ const element = findHostHeader(
283
+ selector,
284
+ own.current?.closest('[data-testid="blog-editor"]') ?? own.current
285
+ );
286
+ if (!element) {
287
+ setHeight(0);
288
+ return;
289
+ }
290
+ const update = () => setHeight(element.offsetHeight);
291
+ update();
292
+ if (typeof ResizeObserver === "undefined") return;
293
+ const observer = new ResizeObserver(update);
294
+ observer.observe(element);
295
+ return () => observer.disconnect();
296
+ }, [selector, offset, own]);
297
+ return offset ?? height;
298
+ }
299
+
300
+ export {
301
+ DEFAULT_TIMINGS,
302
+ resolveTimings,
303
+ DEFAULT_COLUMN,
304
+ DEFAULT_APP_URL,
305
+ BLOG_TOOLBAR_BOTTOM_VAR,
306
+ EDITOR_SCOPE_CLASS,
307
+ designTargetOf,
308
+ designUrl,
309
+ EditDesignLink,
310
+ parsePostSource,
311
+ assemblePostFile,
312
+ canonicalBlock,
313
+ DEFAULT_HEADER_SELECTOR,
314
+ useHeaderOffset
315
+ };
@@ -6,10 +6,13 @@ import {
6
6
  var DEFAULT_API_BASE = "https://api.codepress.dev/v1";
7
7
  var DEFAULT_BASE_PATH = "/blog";
8
8
  var DEFAULT_REVALIDATE_SECONDS = 60;
9
+ var DEFAULT_APP_URL = "https://codepress.dev";
9
10
  function resolveBlog(config) {
10
11
  const origin = new URL(config.baseUrl).origin;
11
12
  const basePath = `/${(config.basePath ?? DEFAULT_BASE_PATH).replace(/^\/+|\/+$/g, "")}`;
12
13
  const name = config.name ?? new URL(origin).host;
14
+ const apiBase = config.apiBase ?? DEFAULT_API_BASE;
15
+ const revalidate = config.revalidate ?? DEFAULT_REVALIDATE_SECONDS;
13
16
  return {
14
17
  site: {
15
18
  baseUrl: `${origin}${basePath}`,
@@ -20,12 +23,19 @@ function resolveBlog(config) {
20
23
  defaultImage: config.defaultImage ?? ""
21
24
  },
22
25
  basePath,
23
- revalidate: config.revalidate ?? DEFAULT_REVALIDATE_SECONDS,
26
+ revalidate,
24
27
  source: createHttpContentSource({
25
- apiBase: config.apiBase ?? DEFAULT_API_BASE,
28
+ apiBase,
26
29
  site: config.site,
27
30
  timeoutMs: config.timeoutMs
28
- })
31
+ }),
32
+ editing: {
33
+ site: config.site,
34
+ apiBase,
35
+ basePath,
36
+ appUrl: config.appUrl ?? DEFAULT_APP_URL,
37
+ revalidate
38
+ }
29
39
  };
30
40
  }
31
41
  function isBuildPhase() {
@@ -45,6 +55,7 @@ export {
45
55
  DEFAULT_API_BASE,
46
56
  DEFAULT_BASE_PATH,
47
57
  DEFAULT_REVALIDATE_SECONDS,
58
+ DEFAULT_APP_URL,
48
59
  resolveBlog,
49
60
  isBuildPhase,
50
61
  listPostsForRender
@@ -65,10 +65,11 @@ function JsonLd({ data }) {
65
65
  function BlogIndexView({
66
66
  blog,
67
67
  posts,
68
- structuredData = true
68
+ structuredData = true,
69
+ editor: Editor
69
70
  }) {
70
71
  const { site, basePath } = blog;
71
- return /* @__PURE__ */ jsxs("main", { className: "cp-blog-index", style: PAGE_STYLE, children: [
72
+ const page = /* @__PURE__ */ jsxs("main", { className: "cp-blog-index", style: PAGE_STYLE, children: [
72
73
  structuredData ? /* @__PURE__ */ jsx(JsonLd, { data: indexJsonLd(site, posts) }) : null,
73
74
  /* @__PURE__ */ jsx("h1", { children: site.title }),
74
75
  /* @__PURE__ */ jsx("p", { style: MUTED_STYLE, children: site.description }),
@@ -78,14 +79,16 @@ function BlogIndexView({
78
79
  /* @__PURE__ */ jsx("p", { children: meta.description })
79
80
  ] }) }, slug)) })
80
81
  ] });
82
+ return Editor ? /* @__PURE__ */ jsx(Editor, { ...blog.editing, children: page }) : page;
81
83
  }
82
84
  function BlogPostView({
83
85
  blog,
84
86
  post,
85
- structuredData = true
87
+ structuredData = true,
88
+ editor: Editor
86
89
  }) {
87
90
  const { meta, html } = post;
88
- return /* @__PURE__ */ jsxs("main", { className: "cp-blog-post", style: PAGE_STYLE, children: [
91
+ const page = /* @__PURE__ */ jsxs("main", { className: "cp-blog-post", style: PAGE_STYLE, children: [
89
92
  structuredData ? postJsonLd(blog.site, post).map((data) => /* @__PURE__ */ jsx(JsonLd, { data }, data)) : null,
90
93
  /* @__PURE__ */ jsxs(Link, { href: blog.basePath, style: MUTED_STYLE, children: [
91
94
  /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u2190" }),
@@ -103,6 +106,7 @@ function BlogPostView({
103
106
  ] }),
104
107
  /* @__PURE__ */ jsx("article", { className: "cp-blog-prose", children: renderPostBody(html) })
105
108
  ] });
109
+ return Editor ? /* @__PURE__ */ jsx(Editor, { ...blog.editing, slug: post.slug, children: page }) : page;
106
110
  }
107
111
 
108
112
  export {
@@ -13,7 +13,7 @@ function createDraftClient({
13
13
  auth
14
14
  }) {
15
15
  const root = apiBase.replace(/\/+$/, "");
16
- const request = async (method, path, body) => {
16
+ const request = async (method, path, body, keepalive = false) => {
17
17
  const headers = { Accept: "application/json" };
18
18
  if (body !== void 0) headers["Content-Type"] = "application/json";
19
19
  const token = await auth.getToken();
@@ -21,7 +21,8 @@ function createDraftClient({
21
21
  const response = await fetch(`${root}${path}`, {
22
22
  method,
23
23
  headers,
24
- body: body === void 0 ? void 0 : JSON.stringify(body)
24
+ body: body === void 0 ? void 0 : JSON.stringify(body),
25
+ keepalive
25
26
  });
26
27
  auth.onResponse?.(response);
27
28
  return response;
@@ -68,12 +69,14 @@ function createDraftClient({
68
69
  return { error: networkMessage };
69
70
  }
70
71
  },
71
- async saveDraft(site, slug, html, baseSha) {
72
+ async saveDraft(site, slug, html, baseSha, options) {
72
73
  try {
73
- const response = await request("PUT", draftPath(site, slug), {
74
- html,
75
- base_sha: baseSha
76
- });
74
+ const response = await request(
75
+ "PUT",
76
+ draftPath(site, slug),
77
+ { html, base_sha: baseSha },
78
+ options?.keepalive
79
+ );
77
80
  if (response.ok) {
78
81
  return { kind: "saved", draft: await response.json() };
79
82
  }
package/dist/cli.js CHANGED
@@ -25,6 +25,8 @@ Options:
25
25
  --base-path <path> Where the blog is mounted (default: /blog)
26
26
  --api-base <url> The CodePress API root (default: https://api.codepress.dev/v1)
27
27
  --revalidate <secs> Seconds between ISR regenerations (default: 60)
28
+ --app-url <url> The CodePress app editors sign in on
29
+ (default: https://codepress.dev)
28
30
  --router <app|pages> Which router to use (default: detected)
29
31
  --force Overwrite the files init writes when they already exist.
30
32
  A /blog route served by another file (the other router,
@@ -36,6 +38,15 @@ Options:
36
38
  var SITE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
37
39
  var PATH_SEGMENT = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
38
40
  var CONFIG_BASENAME = "codepress-blog";
41
+ var EDITOR_BASENAME = "codepress-blog-editor";
42
+ var EDITOR_DEPENDENCIES = {
43
+ "@tiptap/extension-image": "3.24.0",
44
+ "@tiptap/extension-unique-id": "3.24.0",
45
+ "@tiptap/pm": "3.24.0",
46
+ "@tiptap/react": "3.24.0",
47
+ "@tiptap/starter-kit": "3.24.0",
48
+ "@tiptap/suggestion": "3.24.0"
49
+ };
39
50
  var InitError = class extends Error {
40
51
  };
41
52
  function readPackageJson(root) {
@@ -70,9 +81,10 @@ function detectLayout(root, forced) {
70
81
  );
71
82
  }
72
83
  const routesDir = (router === "app" ? app : pages) ?? path.join(root, router);
73
- const allRoutesDirs = ["app", "pages"].flatMap(
74
- (name) => [path.join(root, name), path.join(root, "src", name)].filter(isDirectory).map((dir) => ({ dir, router: name }))
75
- );
84
+ const allRoutesDirs = [
85
+ ...app ? [{ dir: app, router: "app" }] : [],
86
+ ...pages ? [{ dir: pages, router: "pages" }] : []
87
+ ];
76
88
  return {
77
89
  root,
78
90
  router,
@@ -82,7 +94,13 @@ function detectLayout(root, forced) {
82
94
  typescript: existsSync(path.join(root, "tsconfig.json"))
83
95
  };
84
96
  }
85
- var BLOG_ROUTE_KEYS = /* @__PURE__ */ new Set(["", "[]", "feed.xml", "sitemap.xml"]);
97
+ var BLOG_ROUTE_KEYS = /* @__PURE__ */ new Set([
98
+ "",
99
+ "[]",
100
+ "feed.xml",
101
+ "sitemap.xml",
102
+ "__cp/callback"
103
+ ]);
86
104
  function routeKey(segments) {
87
105
  if (segments.some((segment) => segment.startsWith("[[..."))) return "*";
88
106
  return segments.map((segment) => segment.startsWith("[") ? "[]" : segment).join("/");
@@ -95,8 +113,10 @@ function existingRoutes(dir, router, basePath) {
95
113
  const full = path.join(current, entry);
96
114
  if (isDirectory(full)) {
97
115
  if (entry === "node_modules") continue;
116
+ if (router === "app" && entry.startsWith("_")) continue;
98
117
  const invisible = /^\(.*\)$/.test(entry) || entry.startsWith("@");
99
- walk(full, invisible ? url : [...url, entry]);
118
+ const segment = router === "app" ? entry.replace(/%5F/gi, "_") : entry;
119
+ walk(full, invisible ? url : [...url, segment]);
100
120
  continue;
101
121
  }
102
122
  if (!/\.(mdx?|[cm]?[jt]sx?)$/.test(entry)) continue;
@@ -145,6 +165,7 @@ function configSource(layout, options) {
145
165
  options.baseUrl ? ` baseUrl: ${JSON.stringify(options.baseUrl)},` : ` baseUrl: process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000",`,
146
166
  ` basePath: ${JSON.stringify(options.basePath)},`,
147
167
  ...options.apiBase ? [` apiBase: ${JSON.stringify(options.apiBase)},`] : [],
168
+ ...options.appUrl ? [` appUrl: ${JSON.stringify(options.appUrl)},`] : [],
148
169
  ` revalidate: ${options.revalidate},`
149
170
  ];
150
171
  const typed = layout.typescript;
@@ -165,17 +186,36 @@ function routeFiles(layout, options, configFile) {
165
186
  ...options.basePath.split("/").filter(Boolean)
166
187
  );
167
188
  const files = /* @__PURE__ */ new Map();
168
- const add = (file, body) => files.set(file, body(importPath(file, configFile)));
189
+ const add = (file, body) => files.set(file, body(importPath(file, configFile), file));
169
190
  const css = `import "@codepress/blog/blog.css";
170
191
  `;
171
192
  const r = options.revalidate;
193
+ const editorFile = path.join(layout.configDir, `${EDITOR_BASENAME}.${ext}`);
194
+ const editor = (file) => importPath(file, editorFile);
195
+ files.set(
196
+ editorFile,
197
+ `"use client";
198
+ // Written by \`npx @codepress/blog init\`. Editing on this site's own pages,
199
+ // for editors who sign in through CodePress. It renders nothing unless the
200
+ // content site reports host editing enabled. \`@codepress/blog/editor\` also
201
+ // exports helpers, so it is not a client boundary itself: import the editor
202
+ // through this file.
203
+ import "@codepress/blog/editor.css";
204
+
205
+ export {
206
+ HostEditing as BlogEditor,
207
+ HostEditorCallback as BlogEditorCallback,
208
+ } from "@codepress/blog/editor";
209
+ `
210
+ );
172
211
  if (layout.router === "app") {
173
212
  add(
174
213
  path.join(base, `page.${ext}`),
175
- (config) => `${css}import { createAppBlog } from "@codepress/blog/next";
214
+ (config, file) => `${css}import { createAppBlog } from "@codepress/blog/next";
176
215
  import { blogConfig } from "${config}";
216
+ import { BlogEditor } from "${editor(file)}";
177
217
 
178
- const blog = createAppBlog(blogConfig);
218
+ const blog = createAppBlog(blogConfig, { editor: BlogEditor });
179
219
 
180
220
  export const revalidate = ${r};
181
221
  export const generateMetadata = blog.generateIndexMetadata;
@@ -184,16 +224,30 @@ export default blog.IndexPage;
184
224
  );
185
225
  add(
186
226
  path.join(base, "[slug]", `page.${ext}`),
187
- (config) => `${css}import { createAppBlog } from "@codepress/blog/next";
227
+ (config, file) => `${css}import { createAppBlog } from "@codepress/blog/next";
188
228
  import { blogConfig } from "${config}";
229
+ import { BlogEditor } from "${editor(file)}";
189
230
 
190
- const blog = createAppBlog(blogConfig);
231
+ const blog = createAppBlog(blogConfig, { editor: BlogEditor });
191
232
 
192
233
  export const revalidate = ${r};
193
234
  export const dynamicParams = true;
194
235
  export const generateStaticParams = blog.generateStaticParams;
195
236
  export const generateMetadata = blog.generatePostMetadata;
196
237
  export default blog.PostPage;
238
+ `
239
+ );
240
+ add(
241
+ // `%5F` makes the segment `__cp`: a folder starting with `_` is private.
242
+ path.join(base, "%5F_cp", "callback", `page.${ext}`),
243
+ (config, file) => `import { createAppBlog } from "@codepress/blog/next";
244
+ import { blogConfig } from "${config}";
245
+ import { BlogEditorCallback } from "${editor(file)}";
246
+
247
+ const blog = createAppBlog(blogConfig, { callback: BlogEditorCallback });
248
+
249
+ export const metadata = blog.callbackMetadata;
250
+ export default blog.CallbackPage;
197
251
  `
198
252
  );
199
253
  add(
@@ -217,10 +271,11 @@ export const GET = createAppBlogRoutes(blogConfig).sitemap;
217
271
  } else {
218
272
  add(
219
273
  path.join(base, `index.${ext}`),
220
- (config) => `${css}import { createPagesBlog } from "@codepress/blog/next/pages";
274
+ (config, file) => `${css}import { createPagesBlog } from "@codepress/blog/next/pages";
221
275
  import { blogConfig } from "${config}";
276
+ import { BlogEditor } from "${editor(file)}";
222
277
 
223
- const blog = createPagesBlog(blogConfig);
278
+ const blog = createPagesBlog(blogConfig, { editor: BlogEditor });
224
279
 
225
280
  export const getStaticProps = blog.getIndexStaticProps;
226
281
  export default blog.IndexPage;
@@ -228,14 +283,26 @@ export default blog.IndexPage;
228
283
  );
229
284
  add(
230
285
  path.join(base, `[slug].${ext}`),
231
- (config) => `${css}import { createPagesBlog } from "@codepress/blog/next/pages";
286
+ (config, file) => `${css}import { createPagesBlog } from "@codepress/blog/next/pages";
232
287
  import { blogConfig } from "${config}";
288
+ import { BlogEditor } from "${editor(file)}";
233
289
 
234
- const blog = createPagesBlog(blogConfig);
290
+ const blog = createPagesBlog(blogConfig, { editor: BlogEditor });
235
291
 
236
292
  export const getStaticPaths = blog.getPostStaticPaths;
237
293
  export const getStaticProps = blog.getPostStaticProps;
238
294
  export default blog.PostPage;
295
+ `
296
+ );
297
+ add(
298
+ path.join(base, "__cp", `callback.${ext}`),
299
+ (config, file) => `import { createPagesBlog } from "@codepress/blog/next/pages";
300
+ import { blogConfig } from "${config}";
301
+ import { BlogEditorCallback } from "${editor(file)}";
302
+
303
+ const blog = createPagesBlog(blogConfig, { callback: BlogEditorCallback });
304
+
305
+ export default blog.CallbackPage;
239
306
  `
240
307
  );
241
308
  add(
@@ -272,6 +339,7 @@ function parseOptions(argv) {
272
339
  "base-url": { type: "string" },
273
340
  "base-path": { type: "string", default: "/blog" },
274
341
  "api-base": { type: "string" },
342
+ "app-url": { type: "string" },
275
343
  revalidate: { type: "string", default: "60" },
276
344
  router: { type: "string" },
277
345
  force: { type: "boolean", default: false },
@@ -296,7 +364,8 @@ function validate(values) {
296
364
  }
297
365
  for (const [flag, value] of [
298
366
  ["--base-url", values["base-url"]],
299
- ["--api-base", values["api-base"]]
367
+ ["--api-base", values["api-base"]],
368
+ ["--app-url", values["app-url"]]
300
369
  ]) {
301
370
  if (value !== void 0 && !/^https?:\/\/[^/]/.test(value)) {
302
371
  throw new InitError(`${flag} must be an http(s) URL, not "${value}".`);
@@ -313,6 +382,7 @@ function validate(values) {
313
382
  baseUrl: values["base-url"]?.replace(/\/+$/, ""),
314
383
  basePath: `/${segments.join("/")}`,
315
384
  apiBase: values["api-base"],
385
+ appUrl: values["app-url"]?.replace(/\/+$/, ""),
316
386
  revalidate
317
387
  };
318
388
  }
@@ -376,9 +446,18 @@ function runInit(argv, {
376
446
  const dependencies = {
377
447
  ...pkg.dependencies
378
448
  };
379
- const dependencyChanged = !dependencies["@codepress/blog"];
449
+ const devDependencies = pkg.devDependencies ?? {};
450
+ const added = [];
451
+ for (const [name, range] of Object.entries({
452
+ "@codepress/blog": `^${version2}`,
453
+ ...EDITOR_DEPENDENCIES
454
+ })) {
455
+ if (dependencies[name] || devDependencies[name]) continue;
456
+ dependencies[name] = range;
457
+ added.push(`${name}@${range}`);
458
+ }
459
+ const dependencyChanged = added.length > 0;
380
460
  if (dependencyChanged) {
381
- dependencies["@codepress/blog"] = `^${version2}`;
382
461
  pkg.dependencies = Object.fromEntries(
383
462
  Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))
384
463
  );
@@ -395,12 +474,15 @@ function runInit(argv, {
395
474
  if (dependencyChanged) {
396
475
  writeFileSync(pkgFile, `${JSON.stringify(pkg, null, 2)}
397
476
  `);
398
- io.log(` added @codepress/blog@^${version2} to package.json`);
477
+ for (const name of added) io.log(` added ${name} to package.json`);
399
478
  }
400
479
  io.log("");
401
480
  io.log(
402
481
  "The pages render inside your root layout and import @codepress/blog/blog.css."
403
482
  );
483
+ io.log(
484
+ `Editing on these pages shows once your CodePress organization turns on blog host editing and lists this site's origin in the content site's allowed origins. Mark your header with data-blog-header so the editor's toolbar pins below it.`
485
+ );
404
486
  if (dependencyChanged)
405
487
  io.log("Next: install dependencies (npm install, pnpm install or yarn).");
406
488
  if (!options.baseUrl) {