zreader 0.1.0 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ - New `zreader/agent`: `createAgentHandler()` serves agent-readable Markdown twins of every page (`/books/<book>/<chapter>.md`, `/books/<book>.md`, `/books.md`) and `/llms.txt`. MDX is cleaned up and HTML is converted to Markdown.
6
+ - New `zreader/agent/react`: `<CopyReference />` copies a prompt that points an AI agent at the current chapter, with a menu to open it in ChatGPT, Claude or Grok.
7
+ - In `.md` twins, `<embed>`, `<iframe>`, `<object>`, `<video>` and `<audio>` become links to their files (so linked PDFs stay reachable), `<br />` becomes a line break, and MDX that can't be parsed is served as its raw source instead of failing.
8
+ - `<Reader actions>` and the provider's `readerActions` add buttons to the top bar; a chapter's `actions` replaces the provider's, `null` hides them. `useReaderChapter()` also returns `href`.
9
+ - `<ReaderRoot agent>` turns on `<CopyReference markdown />` and a `<link rel="alternate" type="text/markdown">` tag (`<MarkdownAlternate />`) for every chapter. `ReaderRoot` also takes `readerActions`.
10
+ - `withReader()` traces the content directory into the proxy/middleware (webpack).
11
+ - **Breaking:** the Next.js adapter now requires Next.js 15.5 or later.
12
+
3
13
  ## 0.1.0
4
14
 
5
15
  First release.
package/README.md CHANGED
@@ -13,10 +13,12 @@ The core only needs React 19 or later. It works with Vite, React Router, TanStac
13
13
 
14
14
  ```
15
15
  npm i zreader
16
- # optional: the scratchpad editor
16
+ # the scratchpad editor: lazy-loaded, but bundlers resolve it at build time, so install it
17
17
  npm i @tiptap/react @tiptap/pm @tiptap/core @tiptap/starter-kit @tiptap/extension-list @tiptap/extensions @tiptap/markdown
18
18
  ```
19
19
 
20
+ **Full documentation: [docs/](./docs/README.md)**: [getting started](./docs/getting-started.md), [concepts](./docs/concepts.md), [content layer](./docs/content.md), [configuration](./docs/configuration.md), [React](./docs/react.md), [Next.js](./docs/nextjs.md), [reader features](./docs/reader-features.md), [agent-readable content](./docs/agent.md), [Copy Reference](./docs/copy-reference.md), [formats (MDX, HTML, PDF)](./docs/formats.md), [styling](./docs/styling.md), [API reference](./docs/api-reference.md), [troubleshooting](./docs/troubleshooting.md).
21
+
20
22
  ## Entry points
21
23
 
22
24
  | Import | Depends on | What it is |
@@ -30,10 +32,12 @@ npm i @tiptap/react @tiptap/pm @tiptap/core @tiptap/starter-kit @tiptap/extensio
30
32
  | `zreader/next` | Next.js | Optional adapter: `withReader()` for `next.config` |
31
33
  | `zreader/next/client` | Next.js | Optional adapter: `NextReaderProvider` (provider with `next/link`) |
32
34
  | `zreader/next/server` | Next.js | Optional adapter: `ReaderRoot`, `ReaderChapter`, `getBookParams`, `getChapterParams`, `mdxComponents` |
35
+ | `zreader/agent` | Node | Agent-readable Markdown: `createAgentHandler()` serves `.md` twins of every page and `/llms.txt` |
36
+ | `zreader/agent/react` | React | `<CopyReference />`: copy a prompt for an AI agent, or open the page in ChatGPT, Claude or Grok |
33
37
  | `zreader/styles.css` | — | Precompiled stylesheet for apps without Tailwind |
34
38
  | `zreader/tailwind.css` | — | For Tailwind v4 + shadcn apps; your own theme tokens apply |
35
39
 
36
- A build check makes sure the `react`, `editor`, `rehype` and core bundles never import Next.js or Node built-ins.
40
+ A build check makes sure the `react`, `editor`, `agent/react`, `rehype` and core bundles never import Next.js or Node built-ins.
37
41
 
38
42
  ## The provider
39
43
 
@@ -90,7 +94,14 @@ The middle of the top bar takes any content. Set a default on the provider (`hea
90
94
  <Reader book="guide" chapter="intro" header={({ chapter }) => `Now reading: ${chapter.title}`}>…</Reader>
91
95
  ```
92
96
 
93
- `useReaderChapter()` returns `{ book, chapter }` inside a `<Reader>`.
97
+ `useReaderChapter()` returns `{ book, chapter, href }` inside a `<Reader>`.
98
+
99
+ Buttons on the right of the top bar, before the bookmark toggle, work the same way: set a default for every chapter on the provider (`readerActions`), or per chapter with `actions`. A chapter's `actions` replaces the provider's, and `actions={null}` hides them.
100
+
101
+ ```tsx
102
+ <ReaderProvider books={books} readerActions={<CopyReference />}>
103
+ <Reader book="guide" chapter="intro" actions={null}>…</Reader>
104
+ ```
94
105
 
95
106
  ## Rendering MDX directly
96
107
 
@@ -102,6 +113,8 @@ import Intro from "./books/guide/intro.mdx";
102
113
  <Reader book="guide" chapter="intro"><Intro /></Reader>
103
114
  ```
104
115
 
116
+ For what happens to components, expressions and embedded files (such as PDFs) in the reader and in `.md` twins, see [MDX, HTML and PDF: what to expect](#mdx-html-and-pdf-what-to-expect).
117
+
105
118
  Highlights, notes, bookmarks and progress anchor to **block ids**. If the content has none, `<Reader>` adds them in the browser, using exactly the ids the build-time plugin would produce. With no `toc` prop, the chapter's contents list comes from its `h2`/`h3` headings.
106
119
 
107
120
  To have the ids in the HTML from the first paint, add the plugin to your MDX pipeline:
@@ -187,7 +200,7 @@ export const generateStaticParams = getChapterParams;
187
200
  export default async function Page({ params }) { return <ReaderChapter {...await params} />; }
188
201
  ```
189
202
 
190
- - `ReaderRoot` reads `config.reader` from `reader.config.ts` and loads every book. It takes the provider's props except `Link`; pass `books` to override the catalogue.
203
+ - `ReaderRoot` reads `config.reader` from `reader.config.ts` and loads every book. It takes the provider's props except `Link` (including `headerSlot` and `readerActions`); pass `books` to override the catalogue, and `agent` for [agent-readable content](#agent-readable-content-optional).
191
204
  - `ReaderChapter` accepts extra MDX `components`, a per-chapter `header`, and any `<Reader>` prop.
192
205
  - To use your own catalogue instead of the content layer, render `NextReaderProvider` from `next/client` yourself.
193
206
 
@@ -198,6 +211,9 @@ export default async function Page({ params }) { return <ReaderChapter {...await
198
211
  - Keeps jsdom and DOMPurify external.
199
212
  - Traces the content directory into the server output.
200
213
  - Caches compiled chapters in `.next/cache/zreader`.
214
+ - Traces the content directory into the proxy too, for `zreader/agent` (webpack builds; see the note below).
215
+
216
+ > **Turbopack note.** Turbopack does not apply `outputFileTracingIncludes` to the proxy. `next start` and `output: "standalone"` still work, because the page traces include the content directory. On hosts that deploy the proxy as its own function, check that `.md` URLs resolve after deploying.
201
217
 
202
218
  ## Content layer (optional, Node)
203
219
 
@@ -214,6 +230,223 @@ content/books/<book-id>/
214
230
 
215
231
  **Security.** Markdown, inline HTML in Markdown, and `.html` chapters are all sanitized with DOMPurify. That removes scripts, event handlers, `javascript:` URLs, inline styles, forms, and iframes unless you allow them. **MDX is code**: compile only MDX files you trust and author yourself.
216
232
 
233
+ ## Agent-readable content (optional)
234
+
235
+ `zreader/agent` gives every reader page a Markdown twin for LLMs, agents and crawlers, using the content layer (`content/books/**`):
236
+
237
+ | URL | Returns |
238
+ | --- | --- |
239
+ | `/books/<book>/<chapter>.md` | The chapter as Markdown, with previous / contents / next links |
240
+ | `/books/<book>.md` | The book's description and table of contents |
241
+ | `/books.md` | Every book on the shelf |
242
+ | `/llms.txt` | An [llms.txt](https://llmstxt.org) index of every chapter |
243
+
244
+ Markdown chapters are served as written, MDX chapters are cleaned up, and HTML chapters are converted to GFM; see [MDX, HTML and PDF: what to expect](#mdx-html-and-pdf-what-to-expect). The URLs follow `reader.basePath`.
245
+
246
+ `createAgentHandler()` takes a `Request` and returns a `Response`, or `undefined` for any other path. In Next.js (15.5 or later), call it from the proxy:
247
+
248
+ ```ts
249
+ // proxy.ts (Next 16). On Next 15.5, name it middleware.ts, export `middleware`, and add runtime: "nodejs" to config.
250
+ import { NextResponse } from "next/server";
251
+ import { createAgentHandler } from "zreader/agent";
252
+
253
+ const agent = createAgentHandler({ origin: "https://press.example.com" });
254
+ export async function proxy(req: Request) {
255
+ return (await agent(req)) ?? NextResponse.next();
256
+ }
257
+ export const config = { matcher: ["/books/:path*.md", "/books.md", "/llms.txt"] };
258
+ ```
259
+
260
+ > **Keep the matcher in sync with `reader.basePath`.** Next.js reads `matcher` from the file at build time, so it must be a literal; it can't be imported. If you change `basePath` to `/read`, change the matcher to `["/read/:path*.md", "/read.md", "/llms.txt"]` by hand, or every `.md` URL returns 404.
261
+
262
+ Options: `origin` (absolute origin for links; defaults to the `X-Forwarded-Host` / `Host` header, so set it in production), `basePath`, `api` (e.g. a `defineBooks()` content source), `llms` (`false`, or `{ title, description }`), `cacheControl`. Any server that speaks `Request`/`Response` works the same way, such as a React Router resource route or Hono. The converters (`chapterToMarkdown`, `bookToMarkdown`, `shelfToMarkdown`, `llmsTxt`, `mdxToMarkdown`, `htmlToMarkdown`) are exported too.
263
+
264
+ ### Copy Reference
265
+
266
+ `<CopyReference />` from `zreader/agent/react` is a split button for the reader's top bar:
267
+
268
+ - The button copies `Refer the content at <url>`.
269
+ - The menu copies the URL alone, or opens ChatGPT, Claude or Grok with `Read from <url> so I can ask questions about it.`
270
+
271
+ In Next.js, turn on the agent features for every chapter with one flag on `ReaderRoot`:
272
+
273
+ ```tsx
274
+ // app/books/layout.tsx
275
+ <ReaderRoot agent>{children}</ReaderRoot>
276
+ // or customise it (plain values only: built-in providers, strings)
277
+ <ReaderRoot agent={{ prompt: "Use {url} as context", providers: ["chatgpt", "claude"], label: "Cite" }}>{children}</ReaderRoot>
278
+ ```
279
+
280
+ `agent` adds `<CopyReference markdown />` to the top bar of every chapter, plus a `<link rel="alternate" type="text/markdown">` tag in `<head>` (`<MarkdownAlternate />`) so agents and crawlers can find the Markdown twin. Any `readerActions` you pass render after the agent button. It's off by default, because it only works once `proxy.ts` serves the `.md` URLs.
281
+
282
+ Anywhere else, render the components yourself:
283
+
284
+ ```tsx
285
+ import { CopyReference, MarkdownAlternate } from "zreader/agent/react";
286
+
287
+ <ReaderProvider books={books} readerActions={<><MarkdownAlternate /><CopyReference markdown /></>}>
288
+ ```
289
+
290
+ | Prop | Default | |
291
+ | --- | --- | --- |
292
+ | `markdown` | `false` | Reference the page's `.md` twin. Leave it off unless you serve `createAgentHandler()`. A function maps page URLs to Markdown URLs yourself |
293
+ | `prompt` | `"Refer the content at {url}"` | The copied text |
294
+ | `askPrompt` | `"Read from {url} so I can ask questions about it."` | The prompt sent to the assistants |
295
+ | `providers` | `["chatgpt", "claude", "grok"]` | Built-in ids, or `{ id, label, href(prompt) }`. `[]` hides them |
296
+ | `label` | `"Copy reference"` | Button text (shown from the `lg` breakpoint) |
297
+
298
+ The URL is built from the reader's `hrefFor` and `location.origin` when clicked, so it works with custom routes. `markdownUrl`, `formatPrompt` and `providerUrl` are exported for your own buttons.
299
+
300
+ ### Starter: Next.js
301
+
302
+ Chapters live in `content/books/<book>/`. Four files, then every chapter has a `.md` twin, a `<link rel="alternate">` tag and a Copy reference button.
303
+
304
+ ```ts
305
+ // next.config.ts
306
+ import { withReader } from "zreader/next";
307
+ export default withReader({});
308
+ ```
309
+
310
+ ```ts
311
+ // proxy.ts (Next 16). Next 15.5: middleware.ts, export `middleware`, add runtime: "nodejs" to config.
312
+ import { NextResponse } from "next/server";
313
+ import { createAgentHandler } from "zreader/agent";
314
+
315
+ const agent = createAgentHandler({ origin: "https://press.example.com" });
316
+
317
+ export async function proxy(req: Request) {
318
+ return (await agent(req)) ?? NextResponse.next();
319
+ }
320
+ export const config = { matcher: ["/books/:path*.md", "/books.md", "/llms.txt"] };
321
+ ```
322
+
323
+ ```tsx
324
+ // app/books/layout.tsx
325
+ import { ReaderRoot } from "zreader/next/server";
326
+ import "zreader/styles.css";
327
+
328
+ export default function Layout({ children }: { children: React.ReactNode }) {
329
+ return <ReaderRoot agent>{children}</ReaderRoot>;
330
+ }
331
+ ```
332
+
333
+ ```tsx
334
+ // app/books/[book]/[chapter]/page.tsx
335
+ import { getChapterParams, ReaderChapter } from "zreader/next/server";
336
+
337
+ export const generateStaticParams = getChapterParams;
338
+
339
+ export default async function Page({ params }: { params: Promise<{ book: string; chapter: string }> }) {
340
+ return <ReaderChapter {...await params} />;
341
+ }
342
+ ```
343
+
344
+ Result, for `content/books/context-infra/02-appendix-a-api-endpoint-reference.mdx`:
345
+
346
+ - `/books/context-infra/appendix-a-api-endpoint-reference` is the reader page.
347
+ - `/books/context-infra/appendix-a-api-endpoint-reference.md` is its Markdown twin.
348
+ - **Copy reference** copies `Refer the content at https://press.example.com/books/context-infra/appendix-a-api-endpoint-reference.md`.
349
+
350
+ ### Starter: React (Vite)
351
+
352
+ There's no adapter, so you serve the `.md` URLs from your server and add the buttons on the provider. With Vite, a small plugin covers `vite dev` and `vite preview`; in production, put the same handler in front of your static files (Express, Hono, a worker, …).
353
+
354
+ ```ts
355
+ // vite.config.ts
356
+ import { defineConfig, type Plugin } from "vite";
357
+ import react from "@vitejs/plugin-react";
358
+ import { createAgentHandler } from "zreader/agent";
359
+
360
+ function zreaderAgent(): Plugin {
361
+ const handle = createAgentHandler();
362
+ const middleware = async (req: any, res: any, next: () => void) => {
363
+ const response = await handle(new Request(`http://${req.headers.host}${req.url}`, { method: req.method, headers: req.headers }));
364
+ if (!response) return next();
365
+ res.statusCode = response.status;
366
+ response.headers.forEach((value, key) => res.setHeader(key, value));
367
+ res.end(await response.text());
368
+ };
369
+ return {
370
+ name: "zreader-agent",
371
+ configureServer: (server) => void server.middlewares.use(middleware),
372
+ configurePreviewServer: (server) => void server.middlewares.use(middleware),
373
+ };
374
+ }
375
+
376
+ export default defineConfig({ plugins: [react(), zreaderAgent()] });
377
+ ```
378
+
379
+ ```tsx
380
+ // src/main.tsx
381
+ import { createRoot } from "react-dom/client";
382
+ import { BookShelf, Reader, ReaderProvider } from "zreader/react";
383
+ import { CopyReference, MarkdownAlternate } from "zreader/agent/react";
384
+ import "zreader/styles.css";
385
+ import { books, chapters } from "./books"; // your catalogue (BookMeta[]) and chapter components
386
+
387
+ function App() {
388
+ const [, , book, chapter] = window.location.pathname.split("/"); // /books/:book/:chapter
389
+ const Chapter = chapters[`${book}/${chapter}`];
390
+ return (
391
+ <ReaderProvider books={books} readerActions={<><MarkdownAlternate /><CopyReference markdown /></>}>
392
+ {Chapter ? <Reader book={book!} chapter={chapter!}><Chapter /></Reader> : <BookShelf />}
393
+ </ReaderProvider>
394
+ );
395
+ }
396
+
397
+ createRoot(document.getElementById("root")!).render(<App />);
398
+ ```
399
+
400
+ - The handler reads `content/books/**` from disk (and `reader.config.ts` if there is one), so the `.md` twins exist only for chapters in the content layer. Keep the chapters you render and the content folder the same files, e.g. `import.meta.glob("../content/books/**/*.mdx")`.
401
+ - **No server at all** (static hosting only): there is nothing to answer `.md` requests. Use `<CopyReference />` without `markdown`, so it copies the page URL, and skip `<MarkdownAlternate />`. Or point `markdown` at Markdown you host elsewhere: `markdown={(pageUrl) => …}`.
402
+ - With hash routing (`#/read/…`), `.md` URLs can't be derived from the page URL; use the `markdown` function form.
403
+
404
+ ### MDX, HTML and PDF: what to expect
405
+
406
+ The reader page and the `.md` twin are produced separately: the page renders the chapter, the twin converts its source. What survives in each:
407
+
408
+ **Markdown (`.md`)**
409
+
410
+ - Reader: rendered as GFM; inline HTML is sanitized (see Security above).
411
+ - `.md` twin: the file as written, minus frontmatter. A `# Title` from the frontmatter is added unless the chapter already has an H1.
412
+
413
+ **MDX (`.mdx`), through the content layer**
414
+
415
+ | In the source | Reader page | `.md` twin |
416
+ | --- | --- | --- |
417
+ | Markdown, code blocks, tables, task lists | Rendered | Kept |
418
+ | `import` / `export` lines | Run (MDX is code) | Removed |
419
+ | `{expressions}`, `{/* comments */}` | Evaluated | Removed, along with any text they would produce (`## Version {v}` becomes `## Version`) |
420
+ | `<Callout title="Note">…</Callout>` | The `Callout` component | A blockquote with a bold title |
421
+ | Other components with children (`<Tabs>`, `<Steps>`, `<Kbd>`) | Your component | Replaced by their children; props and string attributes are dropped |
422
+ | Self-closing components (`<Chart data={…} />`) | Your component | Removed entirely: anything they render has no Markdown equivalent |
423
+ | `<img src alt>`, `<a href>`, `<br />` | Rendered | Image, link and line break |
424
+ | `<embed>`, `<iframe>`, `<object>`, `<video>`, `<audio>` with a `src`/`data` | Rendered (MDX is not sanitized) | A link to the file, labelled with its `title` or file name, e.g. `[white paper.pdf](/files/white%20paper.pdf)` |
425
+ | Invalid MDX (e.g. an HTML comment `<!-- -->`) | Fails to compile: the build or page errors | The raw source, unconverted |
426
+
427
+ If a component carries content agents need (a diagram's caption, a table built from data), also write it as Markdown in the chapter, or the `.md` twin won't have it.
428
+
429
+ **MDX rendered directly** (`<Reader><Intro /></Reader>`, imported by your bundler)
430
+
431
+ - Reader: works as usual. Block ids, highlights, notes and progress are added in the browser (or at build time with `rehypeBlocks`).
432
+ - `.md` twin: only if the same file is also under `content/books/**`; the handler converts that file. Otherwise there is no twin, so use `<CopyReference />` without `markdown`.
433
+
434
+ **HTML (`.html`)**
435
+
436
+ - Reader: sanitized with DOMPurify (scripts, styles, forms, `<object>` and `<embed>` removed; iframes only from `https` hosts in `sanitize.allowIframes`).
437
+ - `.md` twin: converted to GFM. Scripts and styles are dropped; `<embed>`, `<iframe>` and `<object>` become links to their files.
438
+
439
+ **PDF**
440
+
441
+ zreader reads text chapters only; a PDF is never a chapter.
442
+
443
+ - **A `.pdf` in `content/books/<book>/`** is ignored: it isn't in the catalogue, has no reader page and no `.md` twin (`…/whitepaper.md` returns 404). Widening `loader.pattern` doesn't change that; files of unknown formats are skipped.
444
+ - **Linking to a PDF** (`[Download the spec](/files/spec.pdf)`) works everywhere: the reader shows the link, and the `.md` twin keeps it, so an agent can fetch the PDF itself. Serve the file from your app (e.g. `public/files/`). Relative links resolve the same from the page and from its `.md` twin.
445
+ - **Embedding a PDF**:
446
+ - In `.md` and `.html` chapters, `<embed>` and `<object>` are always removed, and `<iframe>` needs an allow-listed `https` host, so a same-site `/files/spec.pdf` can't be embedded. Link to it instead.
447
+ - In MDX, or as children of `<Reader>`, an `<iframe src="/files/spec.pdf">` renders. The PDF is a black box to the reader: no highlights, notes or bookmarks inside it, no table-of-contents entries, and reading progress only tracks how far the page has scrolled past the frame, not how far into the PDF someone has read. The `.md` twin has a link to the PDF, not its text.
448
+ - **To make a PDF readable** (highlights, progress, a real `.md` twin), convert it to Markdown and add it as a chapter. Keep the PDF as a download link if readers need the original.
449
+
217
450
  ## Styling
218
451
 
219
452
  - **Without Tailwind**, import `styles.css`. It is compiled and unlayered. Theme tokens are namespaced (`--zr-background`, `--zr-primary`, …, `--zr-font-serif`), so you can override them in your own CSS. A scoped reset keeps your site's global element styles out of the reader.
@@ -0,0 +1,90 @@
1
+ import { BookEntry, ChapterEntry, ChapterLookup } from 'zreader/content';
2
+ import { BookMeta } from 'zreader';
3
+
4
+ /** Where the reader lives: absolute origin + the reader's basePath. */
5
+ interface AgentUrlContext {
6
+ /** e.g. "https://press.example.com" (no trailing slash). */
7
+ origin: string;
8
+ /** The reader's basePath, e.g. "/books". */
9
+ basePath: string;
10
+ }
11
+ interface LlmsTxtOptions {
12
+ /** H1 of llms.txt. Defaults to the book title (one book) or "Books". */
13
+ title?: string;
14
+ /** Summary line of llms.txt. */
15
+ description?: string;
16
+ }
17
+ /** Page URL of the shelf, a book, or a chapter. */
18
+ declare function pageUrl(ctx: AgentUrlContext, book?: string, chapter?: string): string;
19
+ /** `.md` URL of the shelf, a book, or a chapter. */
20
+ declare function markdownPageUrl(ctx: AgentUrlContext, book?: string, chapter?: string): string;
21
+ /** Trusted MDX → Markdown: drops imports/exports/expressions, unwraps JSX. */
22
+ declare function mdxToMarkdown(source: string): Promise<string>;
23
+ /** HTML → Markdown (GFM tables, fenced code; embedded files become links). */
24
+ declare function htmlToMarkdown(html: string): Promise<string>;
25
+ /** The chapter's source as Markdown (cached per file + mtime). */
26
+ declare function chapterBodyToMarkdown(entry: Pick<ChapterEntry, "file" | "mtimeMs" | "format" | "body">): Promise<string>;
27
+ /** One chapter as a standalone Markdown document with prev / contents / next links. */
28
+ declare function chapterToMarkdown(hit: ChapterLookup, ctx: AgentUrlContext): Promise<string>;
29
+ /** A book's front matter and table of contents, linking every chapter's `.md`. */
30
+ declare function bookToMarkdown(book: BookMeta | BookEntry, ctx: AgentUrlContext): string;
31
+ /** Every book on the shelf, linking each book's `.md`. */
32
+ declare function shelfToMarkdown(books: Array<BookMeta | BookEntry>, ctx: AgentUrlContext): string;
33
+ /** An llms.txt index (https://llmstxt.org) of every chapter's `.md`. */
34
+ declare function llmsTxt(books: Array<BookMeta | BookEntry>, ctx: AgentUrlContext, opts?: LlmsTxtOptions): string;
35
+
36
+ /**
37
+ * Framework-agnostic `Request → Response` handler that serves agent-readable
38
+ * Markdown next to the reader's HTML pages:
39
+ *
40
+ * {basePath}.md → the shelf
41
+ * {basePath}/:book.md → a book's table of contents
42
+ * {basePath}/:book/:chapter.md → a chapter
43
+ * /llms.txt → an llms.txt index
44
+ *
45
+ * Returns `undefined` for any other request, so it composes with your own
46
+ * middleware / proxy / router.
47
+ */
48
+
49
+ /** The slice of the content API the handler needs (`zreader/content` or `defineBooks()`). */
50
+ interface AgentContentSource {
51
+ getBooks(): Promise<BookEntry[]>;
52
+ getBook(bookId: string): Promise<BookEntry | null>;
53
+ getChapter(bookId: string, chapterId: string): Promise<ChapterLookup | null>;
54
+ }
55
+ interface AgentHandlerOptions {
56
+ /** Defaults to `reader.basePath` from reader.config.ts ("/books"). */
57
+ basePath?: string;
58
+ /** Defaults to the global content layer (`zreader/content`). */
59
+ api?: AgentContentSource;
60
+ /**
61
+ * Absolute origin for links, e.g. "https://press.example.com". Recommended in
62
+ * production. Defaults to the request's `X-Forwarded-Host` / `Host` header.
63
+ */
64
+ origin?: string;
65
+ /** Serve `/llms.txt` (default true), optionally with a custom title/summary. */
66
+ llms?: boolean | LlmsTxtOptions;
67
+ /** Cache-Control header for successful responses. */
68
+ cacheControl?: string;
69
+ }
70
+ type AgentHandler = (request: Request) => Promise<Response | undefined>;
71
+ type Route = {
72
+ kind: "shelf";
73
+ } | {
74
+ kind: "book";
75
+ book: string;
76
+ } | {
77
+ kind: "chapter";
78
+ book: string;
79
+ chapter: string;
80
+ } | {
81
+ kind: "llms";
82
+ };
83
+ /** Match a pathname against the agent routes (pure; exported for tests and custom routers). */
84
+ declare function matchAgentRoute(pathname: string, basePath: string, llms?: boolean): Route | null;
85
+ /** Public origin of a request: forwarded headers first (proxies, `next start`), then its URL. */
86
+ declare function requestOrigin(request: Request): string;
87
+ declare function createAgentHandler(options?: AgentHandlerOptions): AgentHandler;
88
+
89
+ export { bookToMarkdown, chapterBodyToMarkdown, chapterToMarkdown, createAgentHandler, htmlToMarkdown, llmsTxt, markdownPageUrl, matchAgentRoute, mdxToMarkdown, pageUrl, requestOrigin, shelfToMarkdown };
90
+ export type { AgentContentSource, AgentHandler, AgentHandlerOptions, AgentUrlContext, LlmsTxtOptions };