zreader 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 (69) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +467 -2
  4. package/dist/agent/index.d.ts +90 -0
  5. package/dist/agent/index.js +293 -0
  6. package/dist/agent/index.js.map +1 -0
  7. package/dist/agent/react/index.d.ts +65 -0
  8. package/dist/agent/react/index.js +286 -0
  9. package/dist/agent/react/index.js.map +1 -0
  10. package/dist/blocks-DwR7l7pz.d.ts +20 -0
  11. package/dist/chunk-3WQBK6DE.js +47 -0
  12. package/dist/chunk-3WQBK6DE.js.map +1 -0
  13. package/dist/chunk-DYQNGV46.js +31 -0
  14. package/dist/chunk-DYQNGV46.js.map +1 -0
  15. package/dist/chunk-ISI35TCM.js +62 -0
  16. package/dist/chunk-ISI35TCM.js.map +1 -0
  17. package/dist/chunk-RNSAFDZ5.js +105 -0
  18. package/dist/chunk-RNSAFDZ5.js.map +1 -0
  19. package/dist/config/index.cjs +196 -0
  20. package/dist/config/index.cjs.map +1 -0
  21. package/dist/config/index.d.cts +159 -0
  22. package/dist/config/index.d.ts +159 -0
  23. package/dist/config/index.js +182 -0
  24. package/dist/config/index.js.map +1 -0
  25. package/dist/content/index.d.ts +143 -0
  26. package/dist/content/index.js +379 -0
  27. package/dist/content/index.js.map +1 -0
  28. package/dist/core/index.d.ts +262 -0
  29. package/dist/core/index.js +935 -0
  30. package/dist/core/index.js.map +1 -0
  31. package/dist/editor/index.d.ts +19 -0
  32. package/dist/editor/index.js +277 -0
  33. package/dist/editor/index.js.map +1 -0
  34. package/dist/index-Bp5oIiZd.d.ts +61 -0
  35. package/dist/next/client/index.d.ts +8 -0
  36. package/dist/next/client/index.js +13 -0
  37. package/dist/next/client/index.js.map +1 -0
  38. package/dist/next/index.cjs +275 -0
  39. package/dist/next/index.cjs.map +1 -0
  40. package/dist/next/index.d.cts +47 -0
  41. package/dist/next/index.d.ts +47 -0
  42. package/dist/next/index.js +265 -0
  43. package/dist/next/index.js.map +1 -0
  44. package/dist/next/server/index.d.ts +81 -0
  45. package/dist/next/server/index.js +51 -0
  46. package/dist/next/server/index.js.map +1 -0
  47. package/dist/react/index.d.ts +392 -0
  48. package/dist/react/index.js +2798 -0
  49. package/dist/react/index.js.map +1 -0
  50. package/dist/reader.css +116 -0
  51. package/dist/rehype/index.d.ts +27 -0
  52. package/dist/rehype/index.js +4 -0
  53. package/dist/rehype/index.js.map +1 -0
  54. package/dist/stubs/empty-config.d.ts +9 -0
  55. package/dist/stubs/empty-config.js +7 -0
  56. package/dist/stubs/empty-config.js.map +1 -0
  57. package/dist/stubs/server-only.d.ts +2 -0
  58. package/dist/stubs/server-only.js +6 -0
  59. package/dist/stubs/server-only.js.map +1 -0
  60. package/dist/styles.css +1 -0
  61. package/dist/tailwind.css +42 -0
  62. package/dist/types-xkdpkPxA.d.ts +147 -0
  63. package/dist/user-config/index.d.ts +17 -0
  64. package/dist/user-config/index.js +35 -0
  65. package/dist/user-config/index.js.map +1 -0
  66. package/dist/virtual-config/index.d.ts +6 -0
  67. package/dist/virtual-config/index.js +10 -0
  68. package/dist/virtual-config/index.js.map +1 -0
  69. package/package.json +237 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
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
+
13
+ ## 0.1.0
14
+
15
+ First release.
16
+
17
+ - A React reader with a shelf, a contents page per book, and a chapter view.
18
+ - Bookmarks, reading progress and resume, sentence highlights in six colours with notes.
19
+ - A WYSIWYG Markdown scratchpad (Tiptap) per chapter, per book and globally, lazy-loaded from `zreader/editor`.
20
+ - Reading settings: theme (light, dark, sepia), text size, line spacing, measure and font.
21
+ - All reader data stays in `localStorage` under `zr:v1:*`, with JSON backup and restore.
22
+ - The `zreader/rehype` plugin adds block ids at build time; content without ids gets them in the browser.
23
+ - An optional content layer (`zreader/content`) for Markdown, MDX and HTML books.
24
+ - An optional Next.js adapter (`zreader/next`, `zreader/next/client`, `zreader/next/server`).
25
+ - Requires React 19 or later.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alchemyst AI
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 CHANGED
@@ -1,3 +1,468 @@
1
- # Temporary Holding Version
1
+ # zreader
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
+ A localStorage-first, mobile-first book reader for **React**. Readers get:
4
+
5
+ - **Bookmarks** and **reading progress**, which resumes where they left off.
6
+ - **Sentence highlights**: select text, or switch on highlight mode and tap a sentence. Highlights can carry **notes**.
7
+ - A **WYSIWYG Markdown scratchpad** (Tiptap) with checklists. There is a pad per chapter, per book and a global one, and you can quote highlights into it.
8
+ - Reading settings (theme, text size, line spacing and width, font) and JSON **backup and restore**.
9
+
10
+ All reader data stays in the browser. The UI is built from [shadcn/ui](https://ui.shadcn.com) components.
11
+
12
+ The core only needs React 19 or later. It works with Vite, React Router, TanStack, Remix or Next.js. Next.js gets an **optional adapter**.
13
+
14
+ ```
15
+ npm i zreader
16
+ # the scratchpad editor: lazy-loaded, but bundlers resolve it at build time, so install it
17
+ npm i @tiptap/react @tiptap/pm @tiptap/core @tiptap/starter-kit @tiptap/extension-list @tiptap/extensions @tiptap/markdown
18
+ ```
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
+
22
+ ## Entry points
23
+
24
+ | Import | Depends on | What it is |
25
+ | --- | --- | --- |
26
+ | `zreader/react` | React | `ReaderProvider`, `BookShelf`, `BookContents`, `Reader`, hooks |
27
+ | `zreader/editor` | React, Tiptap | The scratchpad editor. The React layer lazy-loads it |
28
+ | `zreader` | — | Core: storage store, anchoring, block ids, backup |
29
+ | `zreader/rehype` | — | `rehypeBlocks` for any MDX/Markdown bundler, so block ids exist before the page loads |
30
+ | `zreader/content` | Node | Optional content layer for `content/books/**` (Markdown, MDX, HTML) |
31
+ | `zreader/config` | — | `defineConfig`, `glob`, `z` |
32
+ | `zreader/next` | Next.js | Optional adapter: `withReader()` for `next.config` |
33
+ | `zreader/next/client` | Next.js | Optional adapter: `NextReaderProvider` (provider with `next/link`) |
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 |
37
+ | `zreader/styles.css` | — | Precompiled stylesheet for apps without Tailwind |
38
+ | `zreader/tailwind.css` | — | For Tailwind v4 + shadcn apps; your own theme tokens apply |
39
+
40
+ A build check makes sure the `react`, `editor`, `agent/react`, `rehype` and core bundles never import Next.js or Node built-ins.
41
+
42
+ ## The provider
43
+
44
+ Everything renders under one `ReaderProvider`. It holds the reader's storage, theme and options, plus the **catalogue** of books. With the catalogue in place, the shelf, contents and reader components only need ids.
45
+
46
+ ```tsx
47
+ import { ReaderProvider } from "zreader/react";
48
+ import "zreader/styles.css";
49
+
50
+ const books = [
51
+ {
52
+ id: "guide",
53
+ title: "The Guide",
54
+ authors: ["A. Writer"],
55
+ chapters: [
56
+ { id: "intro", title: "Introduction", order: 1 },
57
+ { id: "setup", title: "Setup", order: 2, part: "Part I", label: "Chapter 2" },
58
+ ],
59
+ },
60
+ ];
61
+
62
+ <ReaderProvider
63
+ books={books} // the catalogue (BookMeta[]): from JSON, a CMS, a fetch, /content…
64
+ Link={RouterLink} // any router's link; defaults to <a>
65
+ hrefFor={(book, chapter, hash) => …} // defaults to `${basePath}/${book}/${chapter}#hash`
66
+ headerSlot={<Logo />} // middle of the reader's top bar (optional)
67
+ options={{ basePath: "/read", highlightColors: ["yellow", "green"] }}
68
+ >
69
+ …
70
+ </ReaderProvider>
71
+ ```
72
+
73
+ | Component | Needs | Renders |
74
+ | --- | --- | --- |
75
+ | `<BookShelf />` | the provider's `books` | Grid of book cards |
76
+ | `<BookContents book="guide" />` | a book id (or `BookMeta`) | A book's contents page: continue card and chapters grouped by `part` (alias: `BookHome`) |
77
+ | `<ContinueReading book="guide" />`, `<ChapterList book="guide" />` | a book id | The pieces of the contents page |
78
+ | `<Reader book="guide" chapter="intro">…</Reader>` | ids, plus the chapter as children (or `html`) | The full reader |
79
+
80
+ Props always override the provider: `Link`, `hrefFor`, `books`, `header`. `embedded` drops the full-page shell, so the shelf and contents page can sit inside your own layout. An unknown id throws an error that lists the ids it knows.
81
+
82
+ ### The reader's top bar
83
+
84
+ The middle of the top bar takes any content. Set a default on the provider (`headerSlot`) or override it per chapter (`header`). It can be:
85
+
86
+ - a **string**, shown as text;
87
+ - an **element**, such as your logo or a description;
88
+ - a **function** of `{ book, chapter }`.
89
+
90
+ `<ReaderTitle />` is the default book-over-chapter title, so you can combine it with your own content:
91
+
92
+ ```tsx
93
+ <ReaderProvider books={books} headerSlot={<><Logo /><ReaderTitle /></>}>
94
+ <Reader book="guide" chapter="intro" header={({ chapter }) => `Now reading: ${chapter.title}`}>…</Reader>
95
+ ```
96
+
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
+ ```
105
+
106
+ ## Rendering MDX directly
107
+
108
+ Pass the chapter as children: an MDX component from your bundler, JSX, or anything else.
109
+
110
+ ```tsx
111
+ import Intro from "./books/guide/intro.mdx";
112
+
113
+ <Reader book="guide" chapter="intro"><Intro /></Reader>
114
+ ```
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
+
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.
119
+
120
+ To have the ids in the HTML from the first paint, add the plugin to your MDX pipeline:
121
+
122
+ ```ts
123
+ // vite.config.ts
124
+ import mdx from "@mdx-js/rollup";
125
+ import { rehypeBlocks } from "zreader/rehype";
126
+ export default { plugins: [mdx({ rehypePlugins: [rehypeBlocks] })] };
127
+ ```
128
+
129
+ ## Plain React (Vite)
130
+
131
+ ```tsx
132
+ import { BookShelf, BookContents, Reader, ReaderProvider } from "zreader/react";
133
+ import Intro from "./books/guide/intro.mdx";
134
+
135
+ export function App() {
136
+ return (
137
+ <ReaderProvider books={books} hrefFor={(b, c) => (c ? `#/read/${b}/${c}` : `#/read/${b}`)}>
138
+ <BookShelf />
139
+ <BookContents book="guide" />
140
+ <Reader book="guide" chapter="intro"><Intro /></Reader>
141
+ </ReaderProvider>
142
+ );
143
+ }
144
+ ```
145
+
146
+ ## React Router
147
+
148
+ ```tsx
149
+ import { Link, Route, Routes, useParams } from "react-router";
150
+
151
+ function BookPage() {
152
+ return <BookContents book={useParams().book!} />;
153
+ }
154
+ function ChapterPage() {
155
+ const { book, chapter } = useParams();
156
+ const Chapter = chapters[`${book}/${chapter}`]; // e.g. import.meta.glob("./books/**/*.mdx", { eager: true })
157
+ return <Reader book={book!} chapter={chapter!}><Chapter /></Reader>;
158
+ }
159
+
160
+ <ReaderProvider books={books} Link={({ href, ...p }) => <Link to={href} {...p} />} hrefFor={(b, c) => (c ? `/read/${b}/${c}` : `/read/${b}`)}>
161
+ <Routes>
162
+ <Route path="/read" element={<BookShelf />} />
163
+ <Route path="/read/:book" element={<BookPage />} />
164
+ <Route path="/read/:book/:chapter" element={<ChapterPage />} />
165
+ </Routes>
166
+ </ReaderProvider>
167
+ ```
168
+
169
+ ## Next.js (optional adapter)
170
+
171
+ The adapter loads the catalogue on the server, binds `next/link`, and compiles chapters from `content/books/**`. In Next.js, `next/link` has to come from a `"use client"` module; `NextReaderProvider` is that module.
172
+
173
+ ```ts
174
+ // next.config.ts
175
+ import { withReader } from "zreader/next";
176
+ export default withReader({ reactStrictMode: true });
177
+ ```
178
+
179
+ ```tsx
180
+ // app/books/layout.tsx — provider with the whole catalogue and next/link
181
+ import { ReaderRoot } from "zreader/next/server";
182
+ import "zreader/styles.css";
183
+ export default function Layout({ children }) {
184
+ return <ReaderRoot headerSlot={<YourLogo />}>{children}</ReaderRoot>;
185
+ }
186
+
187
+ // app/books/page.tsx
188
+ import { BookShelf } from "zreader/react";
189
+ export default function Page() { return <BookShelf />; }
190
+
191
+ // app/books/[book]/page.tsx
192
+ import { BookContents } from "zreader/react";
193
+ import { getBookParams } from "zreader/next/server";
194
+ export const generateStaticParams = getBookParams;
195
+ export default async function Page({ params }) { return <BookContents book={(await params).book} />; }
196
+
197
+ // app/books/[book]/[chapter]/page.tsx — compiles Markdown/MDX/HTML and renders <Reader>
198
+ import { getChapterParams, ReaderChapter } from "zreader/next/server";
199
+ export const generateStaticParams = getChapterParams;
200
+ export default async function Page({ params }) { return <ReaderChapter {...await params} />; }
201
+ ```
202
+
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).
204
+ - `ReaderChapter` accepts extra MDX `components`, a per-chapter `header`, and any `<Reader>` prop.
205
+ - To use your own catalogue instead of the content layer, render `NextReaderProvider` from `next/client` yourself.
206
+
207
+ `withReader` does the following:
208
+
209
+ - Validates `reader.config.*` at startup.
210
+ - Aliases it for Turbopack and webpack.
211
+ - Keeps jsdom and DOMPurify external.
212
+ - Traces the content directory into the server output.
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.
217
+
218
+ ## Content layer (optional, Node)
219
+
220
+ ```
221
+ content/books/<book-id>/
222
+ book.json { "title", "subtitle"?, "authors"?, "description"?, "chapters"?: [ids] }
223
+ 01-introduction.md frontmatter: title, description?, part?, label?, number?, order?, draft?
224
+ 02-deep-dive.mdx trusted MDX: compiled at build time, may use components
225
+ 03-appendix.html raw HTML, sanitized with DOMPurify
226
+ ```
227
+
228
+ - `getBookMetas()` returns plain `BookMeta[]`, ready for `<ReaderProvider books>` in any framework.
229
+ - `getChapter()` and `renderChapter()` compile one chapter. `renderMarkdown()` and `renderHtml()` handle content from other sources.
230
+
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.
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
+
450
+ ## Styling
451
+
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.
453
+ - **With Tailwind v4 + shadcn**, use `@import "zreader/tailwind.css";` instead. Your own tokens then style the reader.
454
+ - **Themes.** `next-themes`, a plain React library despite the name, sets `light`, `dark` or `sepia` on `<html>` and prevents a flash on load. If you already render a `ThemeProvider`, pass `manageTheme={false}`.
455
+
456
+ ## Storage
457
+
458
+ Keys are namespaced as `zr:v1:*`, and each record is versioned with forward migrations. Corrupt records are dropped item by item, tabs stay in sync, writes are flushed when the page is hidden, and quota errors go to `onStorageError`. `exportBackup` and `importBackup` merge by id, and the most recently updated record wins. You can supply your own `StorageAdapter`, for example IndexedDB.
459
+
460
+ ## Highlights
461
+
462
+ - **Anchors** use W3C-style quote and position selectors, so highlights survive edits to the content. When a passage moves, re-anchoring falls back in order: stored position, then the block's relative offset, then the quote with surrounding context, then a fuzzy match.
463
+ - **Painting** uses the CSS Custom Highlight API, so the DOM is never mutated.
464
+ - **Unplaced highlights** are kept and listed in the panel rather than deleted.
465
+
466
+ ## License
467
+
468
+ MIT © Alchemyst AI
@@ -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 };