@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.
- package/README.md +26 -6
- 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 +100 -18
- 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) {
|
|
@@ -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 = [
|
|
74
|
-
|
|
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([
|
|
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
|
-
|
|
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
|
|
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
|
|
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) {
|