@codepress/blog 0.0.0-stage → 0.2.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.
Files changed (62) hide show
  1. package/README.md +118 -2
  2. package/dist/blog.css +227 -0
  3. package/dist/chunks/BlogEditor-EDAPNNP5.js +2553 -0
  4. package/dist/chunks/chunk-3GCZFCUK.js +51 -0
  5. package/dist/chunks/chunk-3WRTL63O.js +106 -0
  6. package/dist/chunks/chunk-AEQDRBA3.js +128 -0
  7. package/dist/chunks/chunk-AJVVTSKZ.js +90 -0
  8. package/dist/chunks/chunk-EICE5ONB.js +197 -0
  9. package/dist/chunks/chunk-HWR3R5MU.js +48 -0
  10. package/dist/chunks/chunk-QX6276YA.js +0 -0
  11. package/dist/chunks/chunk-TYX2HJZC.js +68 -0
  12. package/dist/chunks/chunk-UA243KAQ.js +104 -0
  13. package/dist/chunks/chunk-WJVZD4UI.js +114 -0
  14. package/dist/chunks/chunk-XLJBCIXI.js +93 -0
  15. package/dist/cli.js +440 -0
  16. package/dist/content-source.d.ts +31 -0
  17. package/dist/editor/BlogEditor.d.ts +40 -0
  18. package/dist/editor/BlogEditorMount.d.ts +71 -0
  19. package/dist/editor/DocumentActions.d.ts +39 -0
  20. package/dist/editor/EditDesignLink.d.ts +30 -0
  21. package/dist/editor/NewPostForm.d.ts +10 -0
  22. package/dist/editor/SlashMenu.d.ts +5 -0
  23. package/dist/editor/Toolbar.d.ts +39 -0
  24. package/dist/editor/blocks.d.ts +5 -0
  25. package/dist/editor/client.d.ts +88 -0
  26. package/dist/editor/client.js +6 -0
  27. package/dist/editor/component-node.d.ts +14 -0
  28. package/dist/editor/config.d.ts +24 -0
  29. package/dist/editor/controller.d.ts +17 -0
  30. package/dist/editor/document.d.ts +47 -0
  31. package/dist/editor/extensions.d.ts +19 -0
  32. package/dist/editor/header.d.ts +20 -0
  33. package/dist/editor/index.d.ts +12 -0
  34. package/dist/editor/index.js +211 -0
  35. package/dist/editor/paste-guard.d.ts +14 -0
  36. package/dist/editor/primitives.d.ts +17 -0
  37. package/dist/editor/props-form.d.ts +17 -0
  38. package/dist/editor/serialize.d.ts +34 -0
  39. package/dist/editor/slash-command.d.ts +21 -0
  40. package/dist/feed.d.ts +4 -0
  41. package/dist/index.d.ts +8 -0
  42. package/dist/index.js +72 -0
  43. package/dist/next/app.d.ts +34 -0
  44. package/dist/next/config.d.ts +44 -0
  45. package/dist/next/index.d.ts +2 -0
  46. package/dist/next/index.js +94 -0
  47. package/dist/next/pages.d.ts +33 -0
  48. package/dist/next/pages.js +167 -0
  49. package/dist/next/routes.d.ts +14 -0
  50. package/dist/next/routes.js +37 -0
  51. package/dist/next/views.d.ts +18 -0
  52. package/dist/react/components.d.ts +18 -0
  53. package/dist/react/index.d.ts +2 -0
  54. package/dist/react/index.js +15 -0
  55. package/dist/react/render.d.ts +7 -0
  56. package/dist/registry.d.ts +95 -0
  57. package/dist/sanitize.d.ts +33 -0
  58. package/dist/schema.d.ts +24 -0
  59. package/dist/seo.d.ts +63 -0
  60. package/dist/sitemap.d.ts +4 -0
  61. package/dist/xml.d.ts +1 -0
  62. package/package.json +92 -4
package/README.md CHANGED
@@ -1,3 +1,119 @@
1
- # Temporary Holding Version
1
+ # @codepress/blog
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ }