@codepress/blog 0.0.0-stage → 0.1.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 +118 -2
- package/dist/blog.css +227 -0
- package/dist/chunks/BlogEditor-VF3UAJB6.js +2527 -0
- package/dist/chunks/chunk-3GCZFCUK.js +51 -0
- package/dist/chunks/chunk-3WRTL63O.js +106 -0
- package/dist/chunks/chunk-AEQDRBA3.js +128 -0
- package/dist/chunks/chunk-AJVVTSKZ.js +90 -0
- package/dist/chunks/chunk-EICE5ONB.js +197 -0
- package/dist/chunks/chunk-HWR3R5MU.js +48 -0
- package/dist/chunks/chunk-QX6276YA.js +0 -0
- package/dist/chunks/chunk-TYX2HJZC.js +68 -0
- package/dist/chunks/chunk-UA243KAQ.js +104 -0
- package/dist/chunks/chunk-WJVZD4UI.js +114 -0
- package/dist/chunks/chunk-XLJBCIXI.js +93 -0
- package/dist/cli.js +440 -0
- package/dist/content-source.d.ts +31 -0
- package/dist/editor/BlogEditor.d.ts +37 -0
- package/dist/editor/BlogEditorMount.d.ts +64 -0
- package/dist/editor/DocumentActions.d.ts +39 -0
- package/dist/editor/EditDesignLink.d.ts +30 -0
- package/dist/editor/NewPostForm.d.ts +10 -0
- package/dist/editor/SlashMenu.d.ts +5 -0
- package/dist/editor/Toolbar.d.ts +39 -0
- package/dist/editor/blocks.d.ts +5 -0
- package/dist/editor/client.d.ts +88 -0
- package/dist/editor/client.js +6 -0
- package/dist/editor/component-node.d.ts +14 -0
- package/dist/editor/config.d.ts +24 -0
- package/dist/editor/document.d.ts +47 -0
- package/dist/editor/extensions.d.ts +19 -0
- package/dist/editor/header.d.ts +20 -0
- package/dist/editor/index.d.ts +11 -0
- package/dist/editor/index.js +209 -0
- package/dist/editor/paste-guard.d.ts +14 -0
- package/dist/editor/primitives.d.ts +17 -0
- package/dist/editor/props-form.d.ts +17 -0
- package/dist/editor/serialize.d.ts +34 -0
- package/dist/editor/slash-command.d.ts +21 -0
- package/dist/feed.d.ts +4 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +72 -0
- package/dist/next/app.d.ts +34 -0
- package/dist/next/config.d.ts +44 -0
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +94 -0
- package/dist/next/pages.d.ts +33 -0
- package/dist/next/pages.js +167 -0
- package/dist/next/routes.d.ts +14 -0
- package/dist/next/routes.js +37 -0
- package/dist/next/views.d.ts +18 -0
- package/dist/react/components.d.ts +18 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +15 -0
- package/dist/react/render.d.ts +7 -0
- package/dist/registry.d.ts +95 -0
- package/dist/sanitize.d.ts +33 -0
- package/dist/schema.d.ts +24 -0
- package/dist/seo.d.ts +63 -0
- package/dist/sitemap.d.ts +4 -0
- package/dist/xml.d.ts +1 -0
- package/package.json +92 -4
package/README.md
CHANGED
|
@@ -1,3 +1,119 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @codepress/blog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Renders a CodePress blog. codepress.dev/blog runs on it, so a site that
|
|
4
|
+
installs it renders posts exactly the same way.
|
|
5
|
+
|
|
6
|
+
## Add a blog to a Next.js site
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx @codepress/blog init --site <your-content-site> --base-url https://your.site
|
|
10
|
+
npm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`init` detects the App or Pages Router and writes `/blog`, `/blog/[slug]`,
|
|
14
|
+
`/blog/feed.xml` and `/blog/sitemap.xml`, plus `codepress-blog.ts` with the
|
|
15
|
+
config. The pages render inside your root layout and import
|
|
16
|
+
`@codepress/blog/blog.css`. It checks both the `app/` and `pages/` trees
|
|
17
|
+
first and changes nothing if `/blog` is already served. `--force` overwrites the
|
|
18
|
+
files `init` itself writes and keeps other `/blog` routes, but a `/blog` URL
|
|
19
|
+
served from a different file (the other router, a route group,
|
|
20
|
+
`pages/blog.tsx`) still stops it: move or delete that file first. Other options: `--base-path`, `--api-base`, `--revalidate`,
|
|
21
|
+
`--router app|pages`. Pages regenerate every 60 seconds (ISR); if the CodePress
|
|
22
|
+
API is down, the last good page keeps being served. Run `npx @codepress/blog --help`
|
|
23
|
+
for the full list.
|
|
24
|
+
|
|
25
|
+
The package is ESM and uses `html-react-parser`, whose dependencies are
|
|
26
|
+
ESM-only. Run Next.js on Node 20.19+ or 22.12+ (these versions can `require()`
|
|
27
|
+
ESM), or add `@codepress/blog` and `html-react-parser` to `transpilePackages`.
|
|
28
|
+
|
|
29
|
+
## Entries
|
|
30
|
+
|
|
31
|
+
- `@codepress/blog` (server-safe, no React)
|
|
32
|
+
- `registry`: the elements, attributes, URL schemes and `cp-*` components a
|
|
33
|
+
post may contain, and the post metadata schema.
|
|
34
|
+
- `sanitize`: `sanitizeHtml` and `sanitizeNodes`, an allowlist derived from
|
|
35
|
+
the registry.
|
|
36
|
+
- `schema`: `buildContentSchema()` and `schemaVersion`, the contract a site
|
|
37
|
+
serves at `<blog>/_schema.json`.
|
|
38
|
+
- `seo`, `feed`, `sitemap`: canonical URLs, JSON-LD, RSS and sitemap
|
|
39
|
+
builders. Each takes a `BlogSite` (`baseUrl`, `name`, `title`,
|
|
40
|
+
`description`, `defaultImage`), so the blog can live at any URL.
|
|
41
|
+
- `createHttpContentSource({ apiBase, site })`: reads published posts from
|
|
42
|
+
the CodePress read API.
|
|
43
|
+
- `@codepress/blog/react`: `renderPostBody(html)` (always sanitizes) and the
|
|
44
|
+
component views, `COMPONENT_VIEWS`.
|
|
45
|
+
- `@codepress/blog/blog.css`: styles for the component views, and the
|
|
46
|
+
optional `.cp-blog-prose` typography for the post body.
|
|
47
|
+
- `@codepress/blog/next`: `createAppBlog(config)` for App Router pages and
|
|
48
|
+
metadata. `@codepress/blog/next/routes`: `createAppBlogRoutes(config)` for
|
|
49
|
+
the feed and sitemap route handlers. `@codepress/blog/next/pages`:
|
|
50
|
+
`createPagesBlog(config)` for the Pages Router.
|
|
51
|
+
- `@codepress/blog/editor` (client-only): the in-place editor.
|
|
52
|
+
`BlogEditorMount` asks `GET {apiBase}/content-sites/{site}/me` and shows an
|
|
53
|
+
Edit button to callers who can edit; the Tiptap editor is a lazy chunk that
|
|
54
|
+
loads on Edit, so importing this entry downloads no ProseMirror code. Props:
|
|
55
|
+
`site`, `slug`, `apiBase`, `auth: { getToken() }` (the only token any draft
|
|
56
|
+
call sends), `header: { selector?, offset? }` (the toolbar pins below
|
|
57
|
+
`[data-blog-header]`, else the page's first `header`, else the top),
|
|
58
|
+
`column`, `onPublished`, `onLeave`, `slots.askPanel`, `slots.designLink` and
|
|
59
|
+
`actions.askCodePress`. `NewPostForm` starts a post (`new` is reserved).
|
|
60
|
+
Tiptap `3.24.0` is a peer dependency. The editor is styled with Tailwind
|
|
61
|
+
utilities: a Tailwind v4 host adds `@source` for this package's
|
|
62
|
+
`src/editor` in this workspace, or `dist/editor` when it installs the
|
|
63
|
+
package from npm.
|
|
64
|
+
- `@codepress/blog/editor/client`: `createDraftClient({ apiBase, auth })`, the
|
|
65
|
+
draft API without the editor.
|
|
66
|
+
|
|
67
|
+
## Example
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import {
|
|
71
|
+
createHttpContentSource,
|
|
72
|
+
postUrl,
|
|
73
|
+
type BlogSite,
|
|
74
|
+
} from "@codepress/blog";
|
|
75
|
+
import { renderPostBody } from "@codepress/blog/react";
|
|
76
|
+
import "@codepress/blog/blog.css";
|
|
77
|
+
|
|
78
|
+
const site: BlogSite = {
|
|
79
|
+
baseUrl: "https://acme.com/blog",
|
|
80
|
+
name: "Acme",
|
|
81
|
+
title: "Acme Blog",
|
|
82
|
+
description: "News from Acme.",
|
|
83
|
+
defaultImage: "https://acme.com/og.png",
|
|
84
|
+
};
|
|
85
|
+
const source = createHttpContentSource({
|
|
86
|
+
apiBase: "https://api.codepress.dev/v1",
|
|
87
|
+
site: "acme",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const post = await source.getPost("hello");
|
|
91
|
+
// <link rel="canonical" href={postUrl(site, "hello")} />
|
|
92
|
+
// <article className="cp-blog-prose">{renderPostBody(post.html)}</article>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Styling
|
|
96
|
+
|
|
97
|
+
Every colour, font and radius in `blog.css` is a CSS variable (`--cp-blog-*`,
|
|
98
|
+
`--cp-callout-*`, `--cp-figure-*`). Override them on `:root` or on an element
|
|
99
|
+
that wraps the post. The defaults are codepress.dev's.
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pnpm --filter @codepress/blog test
|
|
105
|
+
pnpm --filter @codepress/blog typecheck
|
|
106
|
+
pnpm --filter @codepress/blog lint # also forbids apps/web (`@/`) imports
|
|
107
|
+
pnpm --filter @codepress/blog build:package # dist/, as published
|
|
108
|
+
pnpm --filter @codepress/blog test:packaged # init + next build on fixtures/
|
|
109
|
+
node packages/blog/scripts/pack.mjs --dry-run # what npm would publish
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
In the workspace the exports point at `src/`; `publishConfig.exports` holds the
|
|
113
|
+
published `dist/` entries. A new entry in `exports` needs a `publishConfig.exports`
|
|
114
|
+
counterpart, or `pack.mjs` fails. It also fails when a `publishConfig.exports`
|
|
115
|
+
entry has no source export, lacks `types`, or points at a file missing from the
|
|
116
|
+
tarball, and when a declaration file's relative import has no `.js` extension
|
|
117
|
+
(`build.mjs` adds them, so NodeNext TypeScript consumers resolve the types).
|
|
118
|
+
`test:packaged` imports every export from the tarball with Node and type-checks
|
|
119
|
+
a NodeNext consumer against it.
|
package/dist/blog.css
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @codepress/blog styles.
|
|
3
|
+
*
|
|
4
|
+
* Every colour, font and radius is a CSS variable. Override any of them on
|
|
5
|
+
* `:root` or on an element that contains the post to match your site:
|
|
6
|
+
*
|
|
7
|
+
* .blog-post { --cp-blog-font: "Inter", sans-serif; --cp-blog-link: #0a66c2; }
|
|
8
|
+
*
|
|
9
|
+
* `.cp-blog-prose` is optional typography for the post body. Sites that
|
|
10
|
+
* already style articles (for example with Tailwind's `prose`) can skip it;
|
|
11
|
+
* the component styles below stand on their own.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
:where(:root) {
|
|
15
|
+
--cp-blog-font: inherit;
|
|
16
|
+
--cp-blog-heading-font: inherit;
|
|
17
|
+
--cp-blog-mono-font:
|
|
18
|
+
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
19
|
+
--cp-blog-text: #111110;
|
|
20
|
+
--cp-blog-body: #3f3d38;
|
|
21
|
+
--cp-blog-muted: #6b6760;
|
|
22
|
+
--cp-blog-link: #111110;
|
|
23
|
+
--cp-blog-border: #e6e3de;
|
|
24
|
+
--cp-blog-code-bg: #f4f2ee;
|
|
25
|
+
--cp-blog-font-size: 1rem;
|
|
26
|
+
--cp-blog-line-height: 1.75;
|
|
27
|
+
|
|
28
|
+
--cp-callout-radius: calc(0.625rem + 4px);
|
|
29
|
+
--cp-callout-font-size: 15px;
|
|
30
|
+
--cp-callout-text: var(--cp-blog-text);
|
|
31
|
+
--cp-callout-label: var(--cp-blog-muted);
|
|
32
|
+
--cp-callout-info-border: #c7d7f5;
|
|
33
|
+
--cp-callout-info-bg: #f3f7fe;
|
|
34
|
+
--cp-callout-warn-border: #f1d6a3;
|
|
35
|
+
--cp-callout-warn-bg: #fff9ec;
|
|
36
|
+
--cp-callout-tip-border: #bfe3cc;
|
|
37
|
+
--cp-callout-tip-bg: #f1faf4;
|
|
38
|
+
|
|
39
|
+
--cp-figure-radius: 0.625rem;
|
|
40
|
+
--cp-figure-caption: var(--cp-blog-muted);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Post body typography. */
|
|
44
|
+
|
|
45
|
+
.cp-blog-prose {
|
|
46
|
+
color: var(--cp-blog-body);
|
|
47
|
+
font-family: var(--cp-blog-font);
|
|
48
|
+
font-size: var(--cp-blog-font-size);
|
|
49
|
+
line-height: var(--cp-blog-line-height);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.cp-blog-prose :where(p, ul, ol, blockquote, pre, table) {
|
|
53
|
+
margin: 1.25em 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.cp-blog-prose :where(h2, h3, h4) {
|
|
57
|
+
color: var(--cp-blog-text);
|
|
58
|
+
font-family: var(--cp-blog-heading-font);
|
|
59
|
+
font-weight: 600;
|
|
60
|
+
line-height: 1.3;
|
|
61
|
+
margin: 2em 0 0.75em;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.cp-blog-prose :where(h2) {
|
|
65
|
+
font-size: 1.5em;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.cp-blog-prose :where(h3) {
|
|
69
|
+
font-size: 1.25em;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.cp-blog-prose :where(h4) {
|
|
73
|
+
font-size: 1em;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.cp-blog-prose :where(a) {
|
|
77
|
+
color: var(--cp-blog-link);
|
|
78
|
+
font-weight: 500;
|
|
79
|
+
text-decoration: underline;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.cp-blog-prose :where(strong) {
|
|
83
|
+
color: var(--cp-blog-text);
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.cp-blog-prose :where(ul, ol) {
|
|
88
|
+
padding-left: 1.625em;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.cp-blog-prose :where(ul) {
|
|
92
|
+
list-style: disc;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.cp-blog-prose :where(ol) {
|
|
96
|
+
list-style: decimal;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.cp-blog-prose :where(li) {
|
|
100
|
+
margin: 0.5em 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.cp-blog-prose :where(code) {
|
|
104
|
+
color: var(--cp-blog-text);
|
|
105
|
+
font-family: var(--cp-blog-mono-font);
|
|
106
|
+
font-size: 0.875em;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.cp-blog-prose :where(pre) {
|
|
111
|
+
background: var(--cp-blog-code-bg);
|
|
112
|
+
border-radius: 0.375rem;
|
|
113
|
+
overflow-x: auto;
|
|
114
|
+
padding: 0.875em 1.125em;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.cp-blog-prose :where(pre code) {
|
|
118
|
+
font-weight: inherit;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.cp-blog-prose :where(blockquote) {
|
|
122
|
+
border-left: 0.25em solid var(--cp-blog-border);
|
|
123
|
+
color: var(--cp-blog-text);
|
|
124
|
+
font-style: italic;
|
|
125
|
+
padding-left: 1em;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.cp-blog-prose :where(hr) {
|
|
129
|
+
border: 0;
|
|
130
|
+
border-top: 1px solid var(--cp-blog-border);
|
|
131
|
+
margin: 3em 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.cp-blog-prose :where(img) {
|
|
135
|
+
height: auto;
|
|
136
|
+
max-width: 100%;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.cp-blog-prose :where(table) {
|
|
140
|
+
border-collapse: collapse;
|
|
141
|
+
font-size: 0.875em;
|
|
142
|
+
width: 100%;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.cp-blog-prose :where(th, td) {
|
|
146
|
+
border-bottom: 1px solid var(--cp-blog-border);
|
|
147
|
+
padding: 0.5em;
|
|
148
|
+
text-align: left;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.cp-blog-prose :where(th) {
|
|
152
|
+
color: var(--cp-blog-text);
|
|
153
|
+
font-weight: 600;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.cp-blog-prose > :first-child {
|
|
157
|
+
margin-top: 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.cp-blog-prose > :last-child {
|
|
161
|
+
margin-bottom: 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* <cp-callout> */
|
|
165
|
+
|
|
166
|
+
.cp-callout {
|
|
167
|
+
border: 1px solid;
|
|
168
|
+
border-radius: var(--cp-callout-radius);
|
|
169
|
+
box-sizing: border-box;
|
|
170
|
+
color: var(--cp-callout-text);
|
|
171
|
+
font-size: var(--cp-callout-font-size);
|
|
172
|
+
line-height: 1.625;
|
|
173
|
+
margin: 1.5rem 0;
|
|
174
|
+
padding: 1rem 1.25rem;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.cp-callout > * {
|
|
178
|
+
margin: 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.cp-callout--info {
|
|
182
|
+
background: var(--cp-callout-info-bg);
|
|
183
|
+
border-color: var(--cp-callout-info-border);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.cp-callout--warn {
|
|
187
|
+
background: var(--cp-callout-warn-bg);
|
|
188
|
+
border-color: var(--cp-callout-warn-border);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.cp-callout--tip {
|
|
192
|
+
background: var(--cp-callout-tip-bg);
|
|
193
|
+
border-color: var(--cp-callout-tip-border);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.cp-callout .cp-callout__label {
|
|
197
|
+
color: var(--cp-callout-label);
|
|
198
|
+
font-size: 0.75rem;
|
|
199
|
+
font-weight: 600;
|
|
200
|
+
letter-spacing: 0.025em;
|
|
201
|
+
line-height: 1rem;
|
|
202
|
+
margin: 0 0 0.25rem;
|
|
203
|
+
text-transform: uppercase;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* <cp-figure> */
|
|
207
|
+
|
|
208
|
+
.cp-figure {
|
|
209
|
+
margin: 2rem 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.cp-figure .cp-figure__img {
|
|
213
|
+
border-radius: var(--cp-figure-radius);
|
|
214
|
+
display: block;
|
|
215
|
+
height: auto;
|
|
216
|
+
margin: 0;
|
|
217
|
+
max-width: 100%;
|
|
218
|
+
width: 100%;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.cp-figure .cp-figure__caption {
|
|
222
|
+
color: var(--cp-figure-caption);
|
|
223
|
+
font-size: 0.875rem;
|
|
224
|
+
line-height: 1.25rem;
|
|
225
|
+
margin: 0.5rem 0 0;
|
|
226
|
+
text-align: center;
|
|
227
|
+
}
|