@codepress/blog 0.2.1 → 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) {
@@ -83,7 +94,13 @@ function detectLayout(root, forced) {
83
94
  typescript: existsSync(path.join(root, "tsconfig.json"))
84
95
  };
85
96
  }
86
- 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
+ ]);
87
104
  function routeKey(segments) {
88
105
  if (segments.some((segment) => segment.startsWith("[[..."))) return "*";
89
106
  return segments.map((segment) => segment.startsWith("[") ? "[]" : segment).join("/");
@@ -96,8 +113,10 @@ function existingRoutes(dir, router, basePath) {
96
113
  const full = path.join(current, entry);
97
114
  if (isDirectory(full)) {
98
115
  if (entry === "node_modules") continue;
116
+ if (router === "app" && entry.startsWith("_")) continue;
99
117
  const invisible = /^\(.*\)$/.test(entry) || entry.startsWith("@");
100
- walk(full, invisible ? url : [...url, entry]);
118
+ const segment = router === "app" ? entry.replace(/%5F/gi, "_") : entry;
119
+ walk(full, invisible ? url : [...url, segment]);
101
120
  continue;
102
121
  }
103
122
  if (!/\.(mdx?|[cm]?[jt]sx?)$/.test(entry)) continue;
@@ -146,6 +165,7 @@ function configSource(layout, options) {
146
165
  options.baseUrl ? ` baseUrl: ${JSON.stringify(options.baseUrl)},` : ` baseUrl: process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000",`,
147
166
  ` basePath: ${JSON.stringify(options.basePath)},`,
148
167
  ...options.apiBase ? [` apiBase: ${JSON.stringify(options.apiBase)},`] : [],
168
+ ...options.appUrl ? [` appUrl: ${JSON.stringify(options.appUrl)},`] : [],
149
169
  ` revalidate: ${options.revalidate},`
150
170
  ];
151
171
  const typed = layout.typescript;
@@ -166,17 +186,36 @@ function routeFiles(layout, options, configFile) {
166
186
  ...options.basePath.split("/").filter(Boolean)
167
187
  );
168
188
  const files = /* @__PURE__ */ new Map();
169
- const add = (file, body) => files.set(file, body(importPath(file, configFile)));
189
+ const add = (file, body) => files.set(file, body(importPath(file, configFile), file));
170
190
  const css = `import "@codepress/blog/blog.css";
171
191
  `;
172
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
+ );
173
211
  if (layout.router === "app") {
174
212
  add(
175
213
  path.join(base, `page.${ext}`),
176
- (config) => `${css}import { createAppBlog } from "@codepress/blog/next";
214
+ (config, file) => `${css}import { createAppBlog } from "@codepress/blog/next";
177
215
  import { blogConfig } from "${config}";
216
+ import { BlogEditor } from "${editor(file)}";
178
217
 
179
- const blog = createAppBlog(blogConfig);
218
+ const blog = createAppBlog(blogConfig, { editor: BlogEditor });
180
219
 
181
220
  export const revalidate = ${r};
182
221
  export const generateMetadata = blog.generateIndexMetadata;
@@ -185,16 +224,30 @@ export default blog.IndexPage;
185
224
  );
186
225
  add(
187
226
  path.join(base, "[slug]", `page.${ext}`),
188
- (config) => `${css}import { createAppBlog } from "@codepress/blog/next";
227
+ (config, file) => `${css}import { createAppBlog } from "@codepress/blog/next";
189
228
  import { blogConfig } from "${config}";
229
+ import { BlogEditor } from "${editor(file)}";
190
230
 
191
- const blog = createAppBlog(blogConfig);
231
+ const blog = createAppBlog(blogConfig, { editor: BlogEditor });
192
232
 
193
233
  export const revalidate = ${r};
194
234
  export const dynamicParams = true;
195
235
  export const generateStaticParams = blog.generateStaticParams;
196
236
  export const generateMetadata = blog.generatePostMetadata;
197
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;
198
251
  `
199
252
  );
200
253
  add(
@@ -218,10 +271,11 @@ export const GET = createAppBlogRoutes(blogConfig).sitemap;
218
271
  } else {
219
272
  add(
220
273
  path.join(base, `index.${ext}`),
221
- (config) => `${css}import { createPagesBlog } from "@codepress/blog/next/pages";
274
+ (config, file) => `${css}import { createPagesBlog } from "@codepress/blog/next/pages";
222
275
  import { blogConfig } from "${config}";
276
+ import { BlogEditor } from "${editor(file)}";
223
277
 
224
- const blog = createPagesBlog(blogConfig);
278
+ const blog = createPagesBlog(blogConfig, { editor: BlogEditor });
225
279
 
226
280
  export const getStaticProps = blog.getIndexStaticProps;
227
281
  export default blog.IndexPage;
@@ -229,14 +283,26 @@ export default blog.IndexPage;
229
283
  );
230
284
  add(
231
285
  path.join(base, `[slug].${ext}`),
232
- (config) => `${css}import { createPagesBlog } from "@codepress/blog/next/pages";
286
+ (config, file) => `${css}import { createPagesBlog } from "@codepress/blog/next/pages";
233
287
  import { blogConfig } from "${config}";
288
+ import { BlogEditor } from "${editor(file)}";
234
289
 
235
- const blog = createPagesBlog(blogConfig);
290
+ const blog = createPagesBlog(blogConfig, { editor: BlogEditor });
236
291
 
237
292
  export const getStaticPaths = blog.getPostStaticPaths;
238
293
  export const getStaticProps = blog.getPostStaticProps;
239
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;
240
306
  `
241
307
  );
242
308
  add(
@@ -273,6 +339,7 @@ function parseOptions(argv) {
273
339
  "base-url": { type: "string" },
274
340
  "base-path": { type: "string", default: "/blog" },
275
341
  "api-base": { type: "string" },
342
+ "app-url": { type: "string" },
276
343
  revalidate: { type: "string", default: "60" },
277
344
  router: { type: "string" },
278
345
  force: { type: "boolean", default: false },
@@ -297,7 +364,8 @@ function validate(values) {
297
364
  }
298
365
  for (const [flag, value] of [
299
366
  ["--base-url", values["base-url"]],
300
- ["--api-base", values["api-base"]]
367
+ ["--api-base", values["api-base"]],
368
+ ["--app-url", values["app-url"]]
301
369
  ]) {
302
370
  if (value !== void 0 && !/^https?:\/\/[^/]/.test(value)) {
303
371
  throw new InitError(`${flag} must be an http(s) URL, not "${value}".`);
@@ -314,6 +382,7 @@ function validate(values) {
314
382
  baseUrl: values["base-url"]?.replace(/\/+$/, ""),
315
383
  basePath: `/${segments.join("/")}`,
316
384
  apiBase: values["api-base"],
385
+ appUrl: values["app-url"]?.replace(/\/+$/, ""),
317
386
  revalidate
318
387
  };
319
388
  }
@@ -377,9 +446,18 @@ function runInit(argv, {
377
446
  const dependencies = {
378
447
  ...pkg.dependencies
379
448
  };
380
- 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;
381
460
  if (dependencyChanged) {
382
- dependencies["@codepress/blog"] = `^${version2}`;
383
461
  pkg.dependencies = Object.fromEntries(
384
462
  Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))
385
463
  );
@@ -396,12 +474,15 @@ function runInit(argv, {
396
474
  if (dependencyChanged) {
397
475
  writeFileSync(pkgFile, `${JSON.stringify(pkg, null, 2)}
398
476
  `);
399
- io.log(` added @codepress/blog@^${version2} to package.json`);
477
+ for (const name of added) io.log(` added ${name} to package.json`);
400
478
  }
401
479
  io.log("");
402
480
  io.log(
403
481
  "The pages render inside your root layout and import @codepress/blog/blog.css."
404
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
+ );
405
486
  if (dependencyChanged)
406
487
  io.log("Next: install dependencies (npm install, pnpm install or yarn).");
407
488
  if (!options.baseUrl) {
@@ -27,6 +27,8 @@ export interface BlogEditorMountProps {
27
27
  auth: BlogEditorAuth;
28
28
  /** A post that does not exist yet: open the editor straight away. */
29
29
  isNew?: boolean;
30
+ /** Open an existing post's editor straight away instead of showing the Edit button. */
31
+ startEditing?: boolean;
30
32
  /** The host page's header the toolbar pins below. */
31
33
  header?: BlogEditorHeader;
32
34
  /** The reading column's classes; defaults to a 720px column. */
@@ -68,4 +70,4 @@ export declare function appPageUrl(appUrl: string, organizationSlug: string | nu
68
70
  * the answer is `can_edit`; everyone else gets nothing and never downloads the
69
71
  * editor.
70
72
  */
71
- export declare function BlogEditorMount({ site, slug, apiBase, auth, isNew, header, column, appUrl, timings, onEditingChange, onPublished, onLeave, controller, slots, actions, }: BlogEditorMountProps): import("react/jsx-runtime").JSX.Element | null;
73
+ export declare function BlogEditorMount({ site, slug, apiBase, auth, isNew, startEditing, header, column, appUrl, timings, onEditingChange, onPublished, onLeave, controller, slots, actions, }: BlogEditorMountProps): import("react/jsx-runtime").JSX.Element | null;