@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.
- package/README.md +25 -5
- package/dist/chunks/{BlogEditor-EDAPNNP5.js → BlogEditor-IYNL6C7G.js} +166 -219
- package/dist/chunks/chunk-BPAM7T27.js +315 -0
- package/dist/chunks/{chunk-3GCZFCUK.js → chunk-LFSX43CO.js} +14 -3
- package/dist/chunks/{chunk-WJVZD4UI.js → chunk-PPMNLNLD.js} +8 -4
- package/dist/chunks/{chunk-3WRTL63O.js → chunk-WEBQTRDI.js} +10 -7
- package/dist/cli.js +96 -15
- package/dist/editor/BlogEditorMount.d.ts +3 -1
- package/dist/editor/client.d.ts +7 -1
- package/dist/editor/client.js +1 -1
- package/dist/editor/config.d.ts +6 -0
- package/dist/editor/host.d.ts +92 -0
- package/dist/editor/index.d.ts +2 -1
- package/dist/editor/index.js +605 -8
- package/dist/editor.css +3 -0
- package/dist/next/app.d.ts +15 -2
- package/dist/next/config.d.ts +18 -0
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +22 -5
- package/dist/next/pages.d.ts +4 -2
- package/dist/next/pages.js +29 -5
- package/dist/next/routes.js +1 -1
- package/dist/next/views.d.ts +5 -3
- package/package.json +3 -2
- package/dist/chunks/chunk-UA243KAQ.js +0 -104
|
@@ -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
|
|
26
|
+
revalidate,
|
|
24
27
|
source: createHttpContentSource({
|
|
25
|
-
apiBase
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
74
|
-
|
|
75
|
-
|
|
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([
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|