@pokoblog/next 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Disrex V.O.F.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # PokoBlog for Next.js
2
+
3
+ Server components and metadata helpers for rendering a PokoBlog blog in the App
4
+ Router. Built and tested against **Next.js 16.3**.
5
+
6
+ ## Installing
7
+
8
+ ```sh
9
+ npm install @pokoblog/next
10
+ ```
11
+
12
+ Peer dependencies: `next@^16`, `react@^19`.
13
+
14
+ ## Everything here runs on the server
15
+
16
+ That is not a default this package picked; it is the product. These articles
17
+ exist to be found by search engines and by AI crawlers, and GPTBot, ClaudeBot
18
+ and PerplexityBot fetch HTML and read what comes back — they do not run
19
+ JavaScript. A blog fetched in the browser after hydration is an empty div to all
20
+ of them.
21
+
22
+ So there is no `"use client"` anywhere in this package, no hook and no
23
+ `useEffect`, and `./components` imports `server-only`: put these components in a
24
+ client component and the **build fails** rather than shipping a blog nothing can
25
+ read.
26
+
27
+ ## Setup
28
+
29
+ ```ts
30
+ // lib/pokoblog.ts
31
+ import { createPokoBlog } from "@pokoblog/next";
32
+
33
+ export const poko = createPokoBlog({
34
+ url: process.env.POKOBLOG_URL!,
35
+ token: process.env.POKOBLOG_TOKEN!, // Connections → Embed
36
+ });
37
+ ```
38
+
39
+ `fetch` in Next 16 does not cache unless asked, so this asks: `revalidate`
40
+ defaults to 300 seconds, matching the `max-age` PokoBlog sends. Pass
41
+ `revalidate: false` with `tags` if you would rather cache indefinitely and drop
42
+ it from a webhook route handler with `revalidateTag()`.
43
+
44
+ ## The blog index
45
+
46
+ ```tsx
47
+ // app/blog/page.tsx
48
+ import { ArticleList, blogMetadata } from "@pokoblog/next";
49
+
50
+ import { poko } from "@/lib/pokoblog";
51
+
52
+ import type { Metadata } from "next";
53
+
54
+ export const metadata: Metadata = blogMetadata({
55
+ title: "Blog",
56
+ description: "Wat we schrijven over Magento.",
57
+ });
58
+
59
+ export default function BlogIndex() {
60
+ return <ArticleList client={poko} limit={20} />;
61
+ }
62
+ ```
63
+
64
+ One request. A card needs a title, an excerpt, a date and a picture, and the
65
+ list carries all four — there is no call per article here and there must not be
66
+ one.
67
+
68
+ `ArticleList` renders semantic HTML with no styling and no class names of ours,
69
+ because a blog index has to look like the site it is in. Pass `className`, or
70
+ `renderItem` to replace the card entirely; the `<li>` and the paging stay ours.
71
+
72
+ ## One article, with its metadata
73
+
74
+ ```tsx
75
+ // app/blog/[slug]/page.tsx
76
+ import { notFound } from "next/navigation";
77
+
78
+ import {
79
+ ArticleView,
80
+ articleMetadata,
81
+ PokoBlogNotFoundError,
82
+ } from "@pokoblog/next";
83
+
84
+ import { poko } from "@/lib/pokoblog";
85
+
86
+ import type { Metadata } from "next";
87
+
88
+ // `params` is a Promise in Next 16. Awaiting it is not optional.
89
+ type Props = { params: Promise<{ slug: string }> };
90
+
91
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
92
+ const { slug } = await params;
93
+
94
+ try {
95
+ return articleMetadata({
96
+ article: await poko.article(slug),
97
+ url: `https://example.com/blog/${slug}`,
98
+ siteName: "Example",
99
+ locale: "nl_NL",
100
+ });
101
+ } catch (failure) {
102
+ if (failure instanceof PokoBlogNotFoundError) return {};
103
+
104
+ throw failure;
105
+ }
106
+ }
107
+
108
+ export default async function ArticlePage({ params }: Props) {
109
+ const { slug } = await params;
110
+
111
+ try {
112
+ return <ArticleView article={await poko.article(slug)} />;
113
+ } catch (failure) {
114
+ if (failure instanceof PokoBlogNotFoundError) notFound();
115
+
116
+ throw failure;
117
+ }
118
+ }
119
+ ```
120
+
121
+ **Fetching the article twice costs one request.** `fetch` GETs with the same URL
122
+ and options are memoised across `generateMetadata`, layouts and the page within
123
+ one render pass. That is why this package hands you a function of an article
124
+ rather than asking you to thread one from the metadata into the page.
125
+
126
+ `articleMetadata` sets the title, the description, `og:type: article` with
127
+ `article:published_time` and `article:modified_time`, the picture with the alt
128
+ text the author wrote, and a Twitter card sized to whether there is a picture at
129
+ all. It falls back from the meta description to the excerpt — they are different
130
+ fields on purpose, but a page with no description at all gets whatever sentence
131
+ a search engine picks out of the body.
132
+
133
+ ## Static generation
134
+
135
+ ```tsx
136
+ export async function generateStaticParams() {
137
+ return (await poko.slugs()).map((slug) => ({ slug }));
138
+ }
139
+ ```
140
+
141
+ `slugs()` walks every page. The walk is a consistent snapshot: an article
142
+ published while it runs lands in front of the walk and arrives on the next
143
+ build.
144
+
145
+ ## Dropping the cache when PokoBlog publishes
146
+
147
+ ```ts
148
+ // app/api/pokoblog/route.ts
149
+ import { revalidateTag } from "next/cache";
150
+
151
+ export async function POST(request: Request) {
152
+ const body = await request.text(); // raw bytes: the signature is over these
153
+
154
+ // Verify `Poko-Signature` before trusting this. `t=<unix>,v1=<hex>` is
155
+ // HMAC-SHA256 over `<t>.<body>`; compare with `crypto.timingSafeEqual`,
156
+ // never `===`, and refuse anything more than 300 seconds old.
157
+
158
+ revalidateTag("pokoblog", "max");
159
+
160
+ return new Response(null, { status: 204 });
161
+ }
162
+ ```
163
+
164
+ Create the client with `tags: ["pokoblog"]` for this to reach it.
165
+
166
+ **The second argument is not optional in Next 16.** `revalidateTag(tag)` on its
167
+ own is deprecated and does not type-check; `"max"` gives stale-while-revalidate,
168
+ which is what a blog wants. `updateTag(tag)` expires the entry immediately
169
+ instead, at the cost of making the next visitor wait for the refetch.
170
+
171
+ ## The body
172
+
173
+ `ArticleView` renders `article.html` through `dangerouslySetInnerHTML`. That is
174
+ correct here for a specific reason: `html` is the output of PokoBlog's allowlist
175
+ renderer — a closed set of tags, every scrap of text escaped on the way in — and
176
+ is the identical string PokoBlog writes into a WordPress post.
177
+
178
+ **`markdown` is not interchangeable.** It is the unsanitized source and accepts
179
+ raw HTML on purpose, because the renderer escapes it on the way out. Putting it
180
+ through this prop, or through a markdown renderer with raw HTML enabled (which
181
+ is most of them by default), undoes the sanitizing that has already happened.
182
+
183
+ ## Pictures
184
+
185
+ The components use a plain `<img>`, not `next/image`, because `next/image`
186
+ requires the article CDN's hostname in `images.remotePatterns` and a component
187
+ that silently needed a config change would fail in your build with an error
188
+ about a hostname rather than about this package. Once that host is configured,
189
+ swap it in through `renderItem`.
190
+
191
+ `imageAlt` is `null` when nobody wrote alt text, which is **not** the same as
192
+ `alt=""`, and this package never substitutes the title — it describes the
193
+ article, not the picture. The rendered `<img>` uses `alt=""` for the null case,
194
+ which is the honest reading beside a heading carrying the same meaning; the
195
+ metadata omits the attribute entirely.
196
+
197
+ ## The token
198
+
199
+ The token is the **embed** connector's. Rotating it invalidates every URL built
200
+ from the old one, and **disconnecting the embed connector switches this API off
201
+ too** — the widget and the JSON API are the same connector row. Both arrive as a
202
+ `PokoBlogNotFoundError`, which is deliberately indistinguishable from a slug
203
+ that does not exist.
204
+
205
+ ## Tests
206
+
207
+ ```sh
208
+ cd clients/nextjs && npx vitest run
209
+ npx tsc --noEmit
210
+ ```
211
+
212
+ The suite runs under the `react-server` resolve condition, which is how Next
213
+ resolves a server component and the only way to load a module that imports
214
+ `server-only` at all.
@@ -0,0 +1,94 @@
1
+ import type { Article, ArticleBody, ArticlePage } from "./types.js";
2
+ /**
3
+ * Reading a PokoBlog blog from a Next.js server component.
4
+ *
5
+ * ## Everything here runs on the server, and that is the product
6
+ *
7
+ * These articles exist to be found by search engines and by AI crawlers.
8
+ * GPTBot, ClaudeBot and PerplexityBot fetch HTML and read what comes back; they
9
+ * do not run JavaScript. A blog fetched in the browser after hydration is, to
10
+ * every one of them, an empty div -- which is the exact failure the embed
11
+ * widget has and the reason this package exists beside it.
12
+ *
13
+ * So there is no `"use client"` anywhere in this package, no hook, and no
14
+ * `useEffect`. The functions here are `async` and are meant to be awaited
15
+ * inside a server component, which is the App Router default: a component
16
+ * without `"use client"` renders on the server and its output is in the HTML.
17
+ * Nothing has to be configured to get that; it has to be *avoided* to lose it.
18
+ *
19
+ * The one way to lose it is to call these from a component that has
20
+ * `"use client"` at the top. `./components` imports `server-only` so that this
21
+ * fails the build instead of shipping a blog nothing can read.
22
+ *
23
+ * ## Caching
24
+ *
25
+ * `fetch` in Next 16 does not cache unless asked, so this asks: `revalidate`
26
+ * defaults to 300 seconds, matching the `max-age` PokoBlog itself sends. Two
27
+ * consequences worth knowing:
28
+ *
29
+ * - Two calls for the same URL in one render pass are **memoised** into one
30
+ * request. That is what makes calling `article()` in both `generateMetadata`
31
+ * and the page component free, and it is why this package does not ask you to
32
+ * thread the article down from one to the other.
33
+ * - `tags` lets you drop the cache on demand with `revalidateTag()`, which is
34
+ * what to call from a route handler receiving PokoBlog's publish webhook.
35
+ */
36
+ /** The API's own default page size, and its ceiling. Outside 1..100 is a 422. */
37
+ export declare const PAGE = 50;
38
+ export declare const MAX_PAGE = 100;
39
+ export interface PokoBlogOptions {
40
+ /** The origin PokoBlog is served from, e.g. `https://app.example.com`. */
41
+ readonly url: string;
42
+ /** The **embed** connector's token, from Connections → Embed. */
43
+ readonly token: string;
44
+ /**
45
+ * Seconds before a cached answer is refetched. `false` caches indefinitely,
46
+ * which is what a build that revalidates by tag wants; `0` disables caching.
47
+ */
48
+ readonly revalidate?: number | false;
49
+ /** Cache tags, for `revalidateTag()` from a webhook route handler. */
50
+ readonly tags?: readonly string[];
51
+ /** Swappable for tests. Defaults to the global `fetch`. */
52
+ readonly fetch?: typeof globalThis.fetch;
53
+ }
54
+ /** Something the API said no to. `code` is the stable part; branch on it. */
55
+ export declare class PokoBlogError extends Error {
56
+ readonly status: number;
57
+ readonly code: string | null;
58
+ constructor(message: string, status: number, code: string | null);
59
+ }
60
+ /**
61
+ * The address reaches nothing.
62
+ *
63
+ * Deliberately ambiguous at the server and the ambiguity travels: a rotated
64
+ * token, a disconnected embed connector, a slug that is still a draft and a
65
+ * slug that never existed all answer this. That is what stops a stranger
66
+ * confirming a draft's address by asking for it.
67
+ *
68
+ * In a page, this is the one to turn into `notFound()`.
69
+ */
70
+ export declare class PokoBlogNotFoundError extends PokoBlogError {
71
+ constructor(message: string, code: string | null);
72
+ }
73
+ export interface PokoBlogClient {
74
+ /** One page of articles, newest first. */
75
+ readonly page: (options?: {
76
+ readonly limit?: number;
77
+ readonly cursor?: string;
78
+ }) => Promise<ArticlePage>;
79
+ /** Every article, paging handled. Lazy: stop reading and it stops fetching. */
80
+ readonly articles: (options?: {
81
+ readonly perPage?: number;
82
+ }) => AsyncGenerator<Article, void, undefined>;
83
+ /** One article, body and all. */
84
+ readonly article: (slug: string) => Promise<ArticleBody>;
85
+ /** Every slug, for `generateStaticParams`. */
86
+ readonly slugs: () => Promise<string[]>;
87
+ readonly listUrl: (options?: {
88
+ readonly limit?: number;
89
+ readonly cursor?: string;
90
+ }) => string;
91
+ readonly articleUrl: (slug: string) => string;
92
+ }
93
+ export declare const createPokoBlog: ({ url, token, revalidate, tags, fetch: fetcher, }: PokoBlogOptions) => PokoBlogClient;
94
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAY,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,iFAAiF;AACjF,eAAO,MAAM,IAAI,KAAK,CAAC;AACvB,eAAO,MAAM,QAAQ,MAAM,CAAC;AAE5B,MAAM,WAAW,eAAe;IAC9B,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACrC,sEAAsE;IACtE,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAC1C;AAED,6EAA6E;AAC7E,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAE7B,YAAY,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,EAK/D;CACF;AAED;;;;;;;;;GASG;AACH,qBAAa,qBAAsB,SAAQ,aAAa;IACtD,YAAY,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,EAG/C;CACF;AAED,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,EAAE;QACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KAC1B,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE;QAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;KAC3B,KAAK,cAAc,CAAC,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;IAC/C,iCAAiC;IACjC,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IACzD,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE;QAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KAC1B,KAAK,MAAM,CAAC;IACb,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/C;AAED,eAAO,MAAM,cAAc,sDAMxB,eAAe,KAAG,cAoHpB,CAAC"}
package/dist/client.js ADDED
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Reading a PokoBlog blog from a Next.js server component.
3
+ *
4
+ * ## Everything here runs on the server, and that is the product
5
+ *
6
+ * These articles exist to be found by search engines and by AI crawlers.
7
+ * GPTBot, ClaudeBot and PerplexityBot fetch HTML and read what comes back; they
8
+ * do not run JavaScript. A blog fetched in the browser after hydration is, to
9
+ * every one of them, an empty div -- which is the exact failure the embed
10
+ * widget has and the reason this package exists beside it.
11
+ *
12
+ * So there is no `"use client"` anywhere in this package, no hook, and no
13
+ * `useEffect`. The functions here are `async` and are meant to be awaited
14
+ * inside a server component, which is the App Router default: a component
15
+ * without `"use client"` renders on the server and its output is in the HTML.
16
+ * Nothing has to be configured to get that; it has to be *avoided* to lose it.
17
+ *
18
+ * The one way to lose it is to call these from a component that has
19
+ * `"use client"` at the top. `./components` imports `server-only` so that this
20
+ * fails the build instead of shipping a blog nothing can read.
21
+ *
22
+ * ## Caching
23
+ *
24
+ * `fetch` in Next 16 does not cache unless asked, so this asks: `revalidate`
25
+ * defaults to 300 seconds, matching the `max-age` PokoBlog itself sends. Two
26
+ * consequences worth knowing:
27
+ *
28
+ * - Two calls for the same URL in one render pass are **memoised** into one
29
+ * request. That is what makes calling `article()` in both `generateMetadata`
30
+ * and the page component free, and it is why this package does not ask you to
31
+ * thread the article down from one to the other.
32
+ * - `tags` lets you drop the cache on demand with `revalidateTag()`, which is
33
+ * what to call from a route handler receiving PokoBlog's publish webhook.
34
+ */
35
+ /** The API's own default page size, and its ceiling. Outside 1..100 is a 422. */
36
+ export const PAGE = 50;
37
+ export const MAX_PAGE = 100;
38
+ /** Something the API said no to. `code` is the stable part; branch on it. */
39
+ export class PokoBlogError extends Error {
40
+ status;
41
+ code;
42
+ constructor(message, status, code) {
43
+ super(message);
44
+ this.name = "PokoBlogError";
45
+ this.status = status;
46
+ this.code = code;
47
+ }
48
+ }
49
+ /**
50
+ * The address reaches nothing.
51
+ *
52
+ * Deliberately ambiguous at the server and the ambiguity travels: a rotated
53
+ * token, a disconnected embed connector, a slug that is still a draft and a
54
+ * slug that never existed all answer this. That is what stops a stranger
55
+ * confirming a draft's address by asking for it.
56
+ *
57
+ * In a page, this is the one to turn into `notFound()`.
58
+ */
59
+ export class PokoBlogNotFoundError extends PokoBlogError {
60
+ constructor(message, code) {
61
+ super(message, 404, code);
62
+ this.name = "PokoBlogNotFoundError";
63
+ }
64
+ }
65
+ export const createPokoBlog = ({ url, token, revalidate = 300, tags, fetch: fetcher = globalThis.fetch, }) => {
66
+ const base = url.replace(/\/+$/, "");
67
+ if (!/^https?:\/\//i.test(base)) {
68
+ throw new TypeError("PokoBlog: `url` must be an absolute http(s) address");
69
+ }
70
+ if (token.trim() === "") {
71
+ throw new TypeError("PokoBlog: `token` is empty");
72
+ }
73
+ const endpoint = (path) => `${base}/api/connectors/${encodeURIComponent(token)}/${path}`;
74
+ const listUrl = ({ limit = PAGE, cursor, } = {}) => {
75
+ const query = new URLSearchParams({ limit: String(limit) });
76
+ if (cursor !== undefined)
77
+ query.set("cursor", cursor);
78
+ return `${endpoint("articles")}?${query.toString()}`;
79
+ };
80
+ const articleUrl = (slug) => endpoint(`articles/${encodeURIComponent(slug)}`);
81
+ const read = async (target) => {
82
+ const response = await fetcher(target, {
83
+ headers: { accept: "application/json" },
84
+ /*
85
+ * `next` rather than `cache`, because the two conflict: Next ignores both
86
+ * and warns in development when a request sets `revalidate` beside
87
+ * `cache: "no-store"`. One knob, and `revalidate: 0` is how you turn
88
+ * caching off.
89
+ */
90
+ next: { revalidate, ...(tags ? { tags: [...tags] } : {}) },
91
+ });
92
+ if (!response.ok)
93
+ throw await failure(response);
94
+ /*
95
+ * A 200 whose body is not JSON is not a parse error, it is the wrong
96
+ * address answering: a captive portal, a login wall, a load balancer error
97
+ * page, a base URL with a typo in it. Left alone, the caller gets
98
+ * `Unexpected token '<'` from deep inside `Response.json`, which names
99
+ * neither the cause nor the URL -- and the URL is the answer almost every
100
+ * time.
101
+ */
102
+ try {
103
+ return (await response.json());
104
+ }
105
+ catch {
106
+ throw malformed("JSON");
107
+ }
108
+ };
109
+ const page = async (options) => asPage(await read(listUrl(options)));
110
+ async function* articles({ perPage = PAGE, } = {}) {
111
+ let cursor;
112
+ for (;;) {
113
+ const current = await page({
114
+ limit: perPage,
115
+ ...(cursor ? { cursor } : {}),
116
+ });
117
+ yield* current.articles;
118
+ if (current.nextCursor === null)
119
+ return;
120
+ /*
121
+ * A cursor that does not move is an infinite loop, and an infinite loop
122
+ * inside `generateStaticParams` is a build that never finishes. The API
123
+ * cannot produce one -- the cursor is built from the last row of the page
124
+ * -- which is exactly why it is checked rather than trusted: what sits
125
+ * between this and the API is a customer's CDN or a proxy.
126
+ */
127
+ if (current.nextCursor === cursor) {
128
+ throw new PokoBlogError("PokoBlog: the cursor did not advance; refusing to page forever", 200, null);
129
+ }
130
+ cursor = current.nextCursor;
131
+ }
132
+ }
133
+ return {
134
+ page,
135
+ articles,
136
+ article: async (slug) => asArticleBody(await read(articleUrl(slug))),
137
+ slugs: async () => {
138
+ const found = [];
139
+ for await (const article of articles({ perPage: MAX_PAGE })) {
140
+ found.push(article.slug);
141
+ }
142
+ return found;
143
+ },
144
+ listUrl,
145
+ articleUrl,
146
+ };
147
+ };
148
+ const failure = async (response) => {
149
+ /*
150
+ * Read defensively. A 502 usually comes from something in front of the API
151
+ * and its body is usually HTML, so a `json()` that throws must not replace
152
+ * the status the caller needs to see.
153
+ */
154
+ let body = {};
155
+ try {
156
+ body = (await response.json());
157
+ }
158
+ catch {
159
+ body = {};
160
+ }
161
+ const code = typeof body.code === "string" ? body.code : null;
162
+ const described = `PokoBlog answered ${response.status}${code ? ` (${code})` : ""}`;
163
+ return response.status === 404
164
+ ? new PokoBlogNotFoundError(described, code)
165
+ : new PokoBlogError(described, response.status, code);
166
+ };
167
+ /*
168
+ * Narrowing rather than casting.
169
+ *
170
+ * `as ArticlePage` would compile and would be a lie the moment anything but the
171
+ * API answers on that address -- a captive portal, a proxy error page, a base
172
+ * URL with a typo. The first symptom of the cast version is `articles.map is
173
+ * not a function` inside a render, which names neither the cause nor the URL.
174
+ */
175
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
176
+ const malformed = (what) => new PokoBlogError(`PokoBlog: the response is not ${what}; check the base URL`, 200, null);
177
+ const asArticle = (value) => {
178
+ if (!isRecord(value))
179
+ throw malformed("an article");
180
+ const { title, slug, excerpt, description, image, imageAlt, published, modified, } = value;
181
+ if (typeof title !== "string" ||
182
+ typeof slug !== "string" ||
183
+ typeof modified !== "string") {
184
+ throw malformed("an article");
185
+ }
186
+ return {
187
+ title,
188
+ slug,
189
+ excerpt: nullableString(excerpt),
190
+ description: nullableString(description),
191
+ image: nullableString(image),
192
+ imageAlt: nullableString(imageAlt),
193
+ published: nullableString(published),
194
+ modified,
195
+ };
196
+ };
197
+ const asArticleBody = (value) => {
198
+ if (!isRecord(value) || typeof value.html !== "string") {
199
+ throw malformed("an article with a body");
200
+ }
201
+ return {
202
+ ...asArticle(value),
203
+ html: value.html,
204
+ markdown: nullableString(value.markdown),
205
+ };
206
+ };
207
+ const asPage = (value) => {
208
+ if (!isRecord(value) || !Array.isArray(value.articles))
209
+ throw malformed("a listing");
210
+ const { nextCursor } = value;
211
+ if (nextCursor !== null && typeof nextCursor !== "string")
212
+ throw malformed("a listing");
213
+ return {
214
+ articles: value.articles.map(asArticle),
215
+ nextCursor,
216
+ };
217
+ };
218
+ const nullableString = (value) => typeof value === "string" ? value : null;
219
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ import "server-only";
2
+ import type { PokoBlogClient } from "./client.js";
3
+ import type { Article, ArticleBody } from "./types.js";
4
+ import type { ReactNode } from "react";
5
+ /**
6
+ * Server components that put a blog in the HTML.
7
+ *
8
+ * ## `import "server-only"` is the whole point of this file
9
+ *
10
+ * Everything below could be written as a client component that fetches after
11
+ * hydration, and it would look identical in a browser and be worthless. AI
12
+ * crawlers -- GPTBot, ClaudeBot, PerplexityBot -- fetch HTML and read what
13
+ * comes back; they do not run JavaScript. A blog assembled after load is an
14
+ * empty div to every one of them, which defeats the reason these articles are
15
+ * written.
16
+ *
17
+ * A comment asking people not to do that would be a comment. `server-only`
18
+ * makes it a build error: put `"use client"` at the top of a file that imports
19
+ * this one and the build fails with React's own message instead of shipping a
20
+ * blog nothing can read.
21
+ *
22
+ * ## The markup is deliberately plain
23
+ *
24
+ * Semantic HTML, no styling, no class names of ours. A blog index is the part
25
+ * of a customer's site that has to look like their site, and a component with
26
+ * opinions about that is a component people copy out of the package and edit --
27
+ * at which point they own the paging, the dates and the alt text too.
28
+ *
29
+ * The parts that are *not* left to the caller are the ones with a correct
30
+ * answer: `<time dateTime>` in a machine-readable format, alt text that is
31
+ * absent rather than invented, and `html` rather than `markdown` in the body.
32
+ * `renderItem` is there for when the rest is not enough.
33
+ */
34
+ export interface ArticleListProps {
35
+ readonly client: PokoBlogClient;
36
+ /** How many to show. Defaults to the API's page size of 50. */
37
+ readonly limit?: number;
38
+ /** Where an article lives on your site. Defaults to `/blog/<slug>`. */
39
+ readonly href?: (article: Article) => string;
40
+ /** Replace the whole card. The `<li>` is still ours. */
41
+ readonly renderItem?: (article: Article) => ReactNode;
42
+ /** Rendered instead of an empty `<ul>` when there are no articles. */
43
+ readonly empty?: ReactNode;
44
+ readonly className?: string;
45
+ }
46
+ /**
47
+ * The blog index, in the HTML of the response.
48
+ *
49
+ * One request: a card needs a title, an excerpt, a date and a picture, and the
50
+ * list carries all four. There is no per-article call here and there must not
51
+ * be one -- twenty bodies is a megabyte nobody asked for.
52
+ */
53
+ export declare function ArticleList({ client, limit, href, renderItem, empty, className, }: ArticleListProps): Promise<import("react").JSX.Element>;
54
+ export interface ArticleViewProps {
55
+ readonly article: ArticleBody;
56
+ readonly className?: string;
57
+ /** Rendered above the body, after the heading. */
58
+ readonly children?: ReactNode;
59
+ }
60
+ /**
61
+ * One article, body and all, in the HTML of the response.
62
+ *
63
+ * Takes the article rather than fetching it, because the page around it needs
64
+ * the same article for `generateMetadata` and passing it in makes that obvious.
65
+ * (Fetching it here twice would in fact cost one request -- Next memoises `GET`
66
+ * fetches within a render pass -- but a component that quietly relied on that
67
+ * would break the moment somebody wrapped it in a cache with a different key.)
68
+ *
69
+ * ## `dangerouslySetInnerHTML`
70
+ *
71
+ * Named alarmingly and correct here, and the reason is specific rather than
72
+ * general. `html` is the output of PokoBlog's allowlist renderer: a closed set
73
+ * of tags with every scrap of text escaped on the way in, and it is the
74
+ * identical string PokoBlog writes into a WordPress `wp_posts` row. Rendering
75
+ * it is the intended use.
76
+ *
77
+ * `markdown` is **not** interchangeable here. It is the unsanitized source and
78
+ * accepts raw HTML on purpose, because the renderer escapes it on the way out.
79
+ * Putting `markdown` through this prop -- or through a markdown renderer with
80
+ * raw HTML enabled, which is most of them by default -- undoes the sanitizing
81
+ * this field exists to have already done.
82
+ */
83
+ export declare function ArticleView({ article, className, children, }: ArticleViewProps): import("react").JSX.Element;
84
+ //# sourceMappingURL=components.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.tsx"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IAC7C,wDAAwD;IACxD,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,SAAS,CAAC;IACtD,sEAAsE;IACtE,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,EAChC,MAAM,EACN,KAAK,EACL,IAA2C,EAC3C,UAAU,EACV,KAAK,EACL,SAAS,GACV,EAAE,gBAAgB,wCA0ClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,SAAS,EACT,QAAQ,GACT,EAAE,gBAAgB,+BAalB"}
@@ -0,0 +1,60 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "server-only";
3
+ /**
4
+ * The blog index, in the HTML of the response.
5
+ *
6
+ * One request: a card needs a title, an excerpt, a date and a picture, and the
7
+ * list carries all four. There is no per-article call here and there must not
8
+ * be one -- twenty bodies is a megabyte nobody asked for.
9
+ */
10
+ export async function ArticleList({ client, limit, href = (article) => `/blog/${article.slug}`, renderItem, empty, className, }) {
11
+ const { articles } = await client.page(limit === undefined ? {} : { limit });
12
+ if (articles.length === 0 && empty !== undefined)
13
+ return _jsx(_Fragment, { children: empty });
14
+ return (_jsx("ul", { className: className, children: articles.map((article) => (_jsx("li", { children: renderItem ? (renderItem(article)) : (_jsxs("article", { children: [article.image ? (_jsx("img", { src: article.image, alt: article.imageAlt ?? "" })) : null, _jsx("h2", { children: _jsx("a", { href: href(article), children: article.title }) }), _jsx(PublishedAt, { article: article }), article.excerpt ? _jsx("p", { children: article.excerpt }) : null] })) }, article.slug))) }));
15
+ }
16
+ /**
17
+ * One article, body and all, in the HTML of the response.
18
+ *
19
+ * Takes the article rather than fetching it, because the page around it needs
20
+ * the same article for `generateMetadata` and passing it in makes that obvious.
21
+ * (Fetching it here twice would in fact cost one request -- Next memoises `GET`
22
+ * fetches within a render pass -- but a component that quietly relied on that
23
+ * would break the moment somebody wrapped it in a cache with a different key.)
24
+ *
25
+ * ## `dangerouslySetInnerHTML`
26
+ *
27
+ * Named alarmingly and correct here, and the reason is specific rather than
28
+ * general. `html` is the output of PokoBlog's allowlist renderer: a closed set
29
+ * of tags with every scrap of text escaped on the way in, and it is the
30
+ * identical string PokoBlog writes into a WordPress `wp_posts` row. Rendering
31
+ * it is the intended use.
32
+ *
33
+ * `markdown` is **not** interchangeable here. It is the unsanitized source and
34
+ * accepts raw HTML on purpose, because the renderer escapes it on the way out.
35
+ * Putting `markdown` through this prop -- or through a markdown renderer with
36
+ * raw HTML enabled, which is most of them by default -- undoes the sanitizing
37
+ * this field exists to have already done.
38
+ */
39
+ export function ArticleView({ article, className, children, }) {
40
+ return (_jsxs("article", { className: className, children: [_jsx("h1", { children: article.title }), _jsx(PublishedAt, { article: article }), article.image ? (_jsx("img", { src: article.image, alt: article.imageAlt ?? "" })) : null, children, _jsx("div", { dangerouslySetInnerHTML: { __html: article.html } })] }));
41
+ }
42
+ /**
43
+ * `<time dateTime="2026-08-25">25 augustus 2026</time>`, or nothing.
44
+ *
45
+ * The attribute is the machine-readable half and is what a crawler reads; the
46
+ * text is for a person. Both, or the date is only half published -- and the
47
+ * attribute is the ten-character date rather than the full instant, because
48
+ * that is the form `<time>` is defined for and the one search engines parse
49
+ * most reliably.
50
+ *
51
+ * `published` is null for an article with no publish date, and a `<time>` with
52
+ * an empty `dateTime` is invalid markup, so there is nothing to render.
53
+ */
54
+ function PublishedAt({ article }) {
55
+ if (article.published === null)
56
+ return null;
57
+ const date = article.published.slice(0, 10);
58
+ return _jsx("time", { dateTime: date, children: date });
59
+ }
60
+ //# sourceMappingURL=components.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"components.js","sourceRoot":"","sources":["../src/components.tsx"],"names":[],"mappings":";AAAA,OAAO,aAAa,CAAC;AAgDrB;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAChC,MAAM,EACN,KAAK,EACL,IAAI,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,SAAS,OAAO,CAAC,IAAI,EAAE,EAC3C,UAAU,EACV,KAAK,EACL,SAAS,GACQ;IACjB,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAE7E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,4BAAG,KAAK,GAAI,CAAC;IAEtE,OAAO,CACL,aAAI,SAAS,EAAE,SAAS,YACrB,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CACzB,uBACG,UAAU,CAAC,CAAC,CAAC,CACZ,UAAU,CAAC,OAAO,CAAC,CACpB,CAAC,CAAC,CAAC,CACF,8BACG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAgBf,cAAK,GAAG,EAAE,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,GAAI,CACzD,CAAC,CAAC,CAAC,IAAI,EACR,uBACE,YAAG,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,YAAG,OAAO,CAAC,KAAK,GAAK,GACxC,EACL,KAAC,WAAW,IAAC,OAAO,EAAE,OAAO,GAAI,EAChC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,sBAAI,OAAO,CAAC,OAAO,GAAK,CAAC,CAAC,CAAC,IAAI,IAC1C,CACX,IA7BM,OAAO,CAAC,IAAI,CA8BhB,CACN,CAAC,GACC,CACN,CAAC;AACJ,CAAC;AASD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,OAAO,EACP,SAAS,EACT,QAAQ,GACS;IACjB,OAAO,CACL,mBAAS,SAAS,EAAE,SAAS,aAC3B,uBAAK,OAAO,CAAC,KAAK,GAAM,EACxB,KAAC,WAAW,IAAC,OAAO,EAAE,OAAO,GAAI,EAChC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAEf,cAAK,GAAG,EAAE,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,GAAI,CACzD,CAAC,CAAC,CAAC,IAAI,EACP,QAAQ,EACT,cAAK,uBAAuB,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,GAAI,IAClD,CACX,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,WAAW,CAAC,EAAE,OAAO,EAAiC;IAC7D,IAAI,OAAO,CAAC,SAAS,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAE5C,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAE5C,OAAO,eAAM,QAAQ,EAAE,IAAI,YAAG,IAAI,GAAQ,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,8 @@
1
+ export { createPokoBlog, PokoBlogError, PokoBlogNotFoundError, MAX_PAGE, PAGE, } from "./client.js";
2
+ export { ArticleList, ArticleView } from "./components.js";
3
+ export { articleMetadata, blogMetadata } from "./metadata.js";
4
+ export type { PokoBlogClient, PokoBlogOptions } from "./client.js";
5
+ export type { ArticleListProps, ArticleViewProps } from "./components.js";
6
+ export type { ArticleMetadataOptions, BlogMetadataOptions, } from "./metadata.js";
7
+ export type { ApiError, Article, ArticleBody, ArticlePage } from "./types.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,aAAa,EACb,qBAAqB,EACrB,QAAQ,EACR,IAAI,GACL,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE9D,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC1E,YAAY,EACV,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createPokoBlog, PokoBlogError, PokoBlogNotFoundError, MAX_PAGE, PAGE, } from "./client.js";
2
+ export { ArticleList, ArticleView } from "./components.js";
3
+ export { articleMetadata, blogMetadata } from "./metadata.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,aAAa,EACb,qBAAqB,EACrB,QAAQ,EACR,IAAI,GACL,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,51 @@
1
+ import type { Article, ArticleBody } from "./types.js";
2
+ import type { Metadata } from "next";
3
+ /**
4
+ * The `<head>` for one article, built from the article.
5
+ *
6
+ * ## Why this is not a component
7
+ *
8
+ * Metadata in the App Router is a returned object, not markup. Next resolves
9
+ * `generateMetadata` as part of rendering the page and puts the tags in the
10
+ * **initial HTML** -- which is the only version a crawler that does not run
11
+ * JavaScript will ever see, and those are the crawlers this product is for. A
12
+ * component rendering `<meta>` tags into the body would not be in the head, and
13
+ * a client component setting `document.title` would not be in the response at
14
+ * all.
15
+ *
16
+ * ## Calling it costs nothing extra
17
+ *
18
+ * `generateMetadata` and the page component both need the article, and both
19
+ * fetching it is the obvious worry. It is not one: `fetch` GETs with the same
20
+ * URL and options are memoised across `generateMetadata`, layouts and the page
21
+ * within a single render pass, so the second call is the first call's result.
22
+ * That is why this package hands you a function of an article rather than
23
+ * asking you to thread one down from the metadata into the page.
24
+ */
25
+ export interface ArticleMetadataOptions {
26
+ /** The article. Either shape -- the body is not used. */
27
+ readonly article: Article | ArticleBody;
28
+ /**
29
+ * The canonical address of this page on *your* site.
30
+ *
31
+ * Absolute, or relative to the `metadataBase` in your root layout. Worth
32
+ * passing: it is what stops the same article being indexed separately under
33
+ * every query string that reaches it.
34
+ */
35
+ readonly url?: string;
36
+ /** Your site's name, for `og:site_name`. */
37
+ readonly siteName?: string;
38
+ /** e.g. `"nl_NL"`. */
39
+ readonly locale?: string;
40
+ }
41
+ export declare const articleMetadata: ({ article, url, siteName, locale, }: ArticleMetadataOptions) => Metadata;
42
+ export interface BlogMetadataOptions {
43
+ readonly title: string;
44
+ readonly description?: string;
45
+ readonly url?: string;
46
+ readonly siteName?: string;
47
+ readonly locale?: string;
48
+ }
49
+ /** The `<head>` for the index page. `website`, not `article`. */
50
+ export declare const blogMetadata: ({ title, description, url, siteName, locale, }: BlogMetadataOptions) => Metadata;
51
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,sBAAsB;IACrC,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,CAAC;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,sBAAsB;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,eAAe,wCAKzB,sBAAsB,KAAG,QAgE3B,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,iEAAiE;AACjE,eAAO,MAAM,YAAY,mDAMtB,mBAAmB,KAAG,QAYvB,CAAC"}
@@ -0,0 +1,78 @@
1
+ export const articleMetadata = ({ article, url, siteName, locale, }) => {
2
+ /*
3
+ * The meta description if there is one, the excerpt if there is not.
4
+ *
5
+ * They are different fields on purpose -- the description is written for a
6
+ * search result and the excerpt is the line under a title in a list -- so
7
+ * this is a fallback and not an equivalence. It exists because the failure it
8
+ * prevents is worse than the imprecision it accepts: a page with no
9
+ * description at all gets whatever sentence the search engine picks out of
10
+ * the body, and for an article whose first line is a heading that is usually
11
+ * the heading.
12
+ */
13
+ const description = article.description ?? article.excerpt ?? undefined;
14
+ /*
15
+ * The alt travels with the image and is not invented when it is missing.
16
+ *
17
+ * `imageAlt: null` means nobody has written alt text, which is not the same
18
+ * as `alt=""`. Substituting the title here would describe the article rather
19
+ * than the picture, in the one place a screen reader is most likely to read
20
+ * it out.
21
+ */
22
+ const images = article.image
23
+ ? [
24
+ {
25
+ url: article.image,
26
+ ...(article.imageAlt === null ? {} : { alt: article.imageAlt }),
27
+ },
28
+ ]
29
+ : undefined;
30
+ return {
31
+ title: article.title,
32
+ ...(description ? { description } : {}),
33
+ ...(url ? { alternates: { canonical: url } } : {}),
34
+ openGraph: {
35
+ /*
36
+ * `article`, not `website`. It is what carries `article:published_time`
37
+ * and `article:modified_time`, and those are how a reader -- or a model
38
+ * summarising the page -- knows whether they are looking at something
39
+ * from this week or from 2019.
40
+ */
41
+ type: "article",
42
+ title: article.title,
43
+ ...(description ? { description } : {}),
44
+ ...(url ? { url } : {}),
45
+ ...(siteName ? { siteName } : {}),
46
+ ...(locale ? { locale } : {}),
47
+ ...(article.published ? { publishedTime: article.published } : {}),
48
+ modifiedTime: article.modified,
49
+ ...(images ? { images } : {}),
50
+ },
51
+ twitter: {
52
+ /*
53
+ * `summary_large_image` when there is a picture and `summary` when there
54
+ * is not. Claiming the large card without an image gets a card with a
55
+ * blank rectangle where the picture should be.
56
+ */
57
+ card: article.image ? "summary_large_image" : "summary",
58
+ title: article.title,
59
+ ...(description ? { description } : {}),
60
+ ...(images ? { images } : {}),
61
+ },
62
+ };
63
+ };
64
+ /** The `<head>` for the index page. `website`, not `article`. */
65
+ export const blogMetadata = ({ title, description, url, siteName, locale, }) => ({
66
+ title,
67
+ ...(description ? { description } : {}),
68
+ ...(url ? { alternates: { canonical: url } } : {}),
69
+ openGraph: {
70
+ type: "website",
71
+ title,
72
+ ...(description ? { description } : {}),
73
+ ...(url ? { url } : {}),
74
+ ...(siteName ? { siteName } : {}),
75
+ ...(locale ? { locale } : {}),
76
+ },
77
+ });
78
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The shapes `GET /api/connectors/:token/articles` actually answers with.
3
+ *
4
+ * Written from responses that were recorded off the running API rather than
5
+ * from the server's own types: a type copied across agrees with the server it
6
+ * was copied from and with nothing else, and the day the two repositories drift
7
+ * is the day the copy stops being a check on anything.
8
+ *
9
+ * Every field is `readonly`. These objects come out of a network response and
10
+ * nothing downstream has any business editing one in place -- a component that
11
+ * did would be editing an object another component is also rendering, since a
12
+ * `fetch` inside one render pass is memoised and hands the same value to
13
+ * everybody who asked.
14
+ */
15
+ /** One published article, as the list answers. No body -- see {@link ArticleBody}. */
16
+ export interface Article {
17
+ readonly title: string;
18
+ readonly slug: string;
19
+ /** The line under the title in a list. Not the meta description. */
20
+ readonly excerpt: string | null;
21
+ /** The sentence written for a search result. Not the excerpt. */
22
+ readonly description: string | null;
23
+ readonly image: string | null;
24
+ /**
25
+ * The alt text, and `null` when there is none.
26
+ *
27
+ * `null` is not `alt=""`. The empty string tells a screen reader the picture
28
+ * is decoration, which is a claim the article's author did not make. Decide
29
+ * once, in your own component, what to do when it is null.
30
+ */
31
+ readonly imageAlt: string | null;
32
+ /** ISO 8601, UTC. */
33
+ readonly published: string | null;
34
+ /** ISO 8601, UTC. The row's last write, which a publish also moves. */
35
+ readonly modified: string;
36
+ }
37
+ /**
38
+ * One article with its body.
39
+ *
40
+ * **`html` is the field to render.** It is the output of PokoBlog's allowlist
41
+ * renderer and is the identical string PokoBlog writes into a WordPress post.
42
+ *
43
+ * **`markdown` is the source and it is not sanitized.** It is here for
44
+ * consumers that genuinely re-render -- an MDX pipeline, a native app, a search
45
+ * index -- and any renderer pointed at it must have raw HTML disabled. It is
46
+ * `null` for an article written before the field existed.
47
+ */
48
+ export interface ArticleBody extends Article {
49
+ readonly html: string;
50
+ readonly markdown: string | null;
51
+ }
52
+ /** One page of the list, and where the next one starts. */
53
+ export interface ArticlePage {
54
+ readonly articles: readonly Article[];
55
+ /** `null` on the last page, and never absent. */
56
+ readonly nextCursor: string | null;
57
+ }
58
+ /** The error body every non-2xx on this API carries. */
59
+ export interface ApiError {
60
+ readonly code: string;
61
+ readonly message: string;
62
+ readonly status: number;
63
+ }
64
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,sFAAsF;AACtF,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,iEAAiE;IACjE,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,qBAAqB;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,WAAY,SAAQ,OAAO;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,2DAA2D;AAC3D,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,iDAAiD;IACjD,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,wDAAwD;AACxD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB"}
package/dist/types.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The shapes `GET /api/connectors/:token/articles` actually answers with.
3
+ *
4
+ * Written from responses that were recorded off the running API rather than
5
+ * from the server's own types: a type copied across agrees with the server it
6
+ * was copied from and with nothing else, and the day the two repositories drift
7
+ * is the day the copy stops being a check on anything.
8
+ *
9
+ * Every field is `readonly`. These objects come out of a network response and
10
+ * nothing downstream has any business editing one in place -- a component that
11
+ * did would be editing an object another component is also rendering, since a
12
+ * `fetch` inside one render pass is memoised and hands the same value to
13
+ * everybody who asked.
14
+ */
15
+ export {};
16
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@pokoblog/next",
3
+ "version": "1.0.0",
4
+ "description": "Server components and metadata helpers for rendering a PokoBlog blog in Next.js.",
5
+ "license": "MIT",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "type": "module",
10
+ "sideEffects": false,
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./client": {
19
+ "types": "./dist/client.d.ts",
20
+ "default": "./dist/client.js"
21
+ },
22
+ "./metadata": {
23
+ "types": "./dist/metadata.d.ts",
24
+ "default": "./dist/metadata.js"
25
+ }
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.build.json",
29
+ "test": "vitest run",
30
+ "typecheck": "tsc --noEmit",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "dependencies": {
34
+ "server-only": "0.0.1"
35
+ },
36
+ "peerDependencies": {
37
+ "next": "^16.0.0",
38
+ "react": "^19.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^24.0.0",
42
+ "@types/react": "19.2.18",
43
+ "next": "16.3.1",
44
+ "react": "19.2.8",
45
+ "react-dom": "19.2.8",
46
+ "typescript": "7.0.2",
47
+ "vitest": "4.1.10"
48
+ }
49
+ }