seemore 1.1.4 → 1.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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <br/>
2
2
 
3
3
  <p align="center">
4
- <img src="https://raw.githubusercontent.com/arifszn/seemore/main/packages/vscode/assets/icon.png" alt="seemore" width="40" height="40">
4
+ <img src="https://raw.githubusercontent.com/arifszn/seemore/main/assets/icon.png" alt="seemore" width="40" height="40">
5
5
  <h1 align="center">seemore</h1>
6
6
  <h4 align="center">Let AI write the Markdown. Let seemore show it better — zero config documentation framework.</h4>
7
7
  <p align="center">
@@ -42,11 +42,9 @@
42
42
  </p>
43
43
  </p>
44
44
 
45
- AI tools write Markdown — specs, notes, guides, READMEs, whole folders of it, faster than
46
- anyone can read. A pile of `.md` files is write-only memory: nothing to click, nothing to
47
- search, no order.
45
+ AI tools write Markdown — specs, notes, guides, READMEs, whole folders of it, faster than anyone can read. A pile of `.md` files is write-only memory: nothing to click, nothing to search, no order.
48
46
 
49
- seemore points at that folder and renders it as a proper site instead, without you moving a single file or writing any code.
47
+ **seemore** points at that folder and renders it as a proper site instead, without you moving a single file or writing any code.
50
48
 
51
49
  Three ways to use it:
52
50
 
@@ -54,26 +52,39 @@ Three ways to use it:
54
52
  - **[In your code editor](#view-in-your-code-editor)**: an extension puts the same site in a panel next to the file you're editing — VS Code and VS Code-compatible editors like Cursor and Antigravity.
55
53
  - **[As a static site](#publish-it-to-the-web)**: `npx seemore build` exports plain HTML you can host anywhere, so it doubles as a docs framework, not just a preview tool.
56
54
 
55
+ And whichever preview is open, the page is also an editor: **double-click any paragraph to fix its Markdown in place**, and the change is written straight back to the file.
56
+
57
57
  <p align="center">
58
- <img src="https://raw.githubusercontent.com/arifszn/seemore/main/packages/site/assets/themes/neutral.png" alt="seemore rendering a folder of Markdown in the browser, neutral theme" width="640"/>
58
+ <img src="https://raw.githubusercontent.com/arifszn/seemore/main/packages/site/assets/inline-editor.png" alt="seemore rendering a folder of Markdown in the browser, with a paragraph's Markdown source open in the inline editor" width="640"/>
59
59
  </p>
60
60
 
61
+ ## Features
62
+
63
+ - **Zero config** — no config file, no code, no files to move; a plain folder of Markdown works in the browser, in your editor, and as a static build
64
+ - **Live preview** — add, rename, retitle or delete a file and the site updates immediately, navigation and search included
65
+ - **Edit in place** — double-click any block in the preview to fix its Markdown; saves are surgical, so `git diff` shows the sentence you changed and nothing else
66
+ - **Static export** — `seemore build` prerenders every page to its own HTML file, `404.html` included, and adds the conventions individual hosts look for (`_redirects`, `200.html`, `.nojekyll`)
67
+ - **Search built in** — static, zero-setup full-text search out of the box, with shareable highlighted results; [Algolia](https://algolia.com) and [Orama Cloud](https://orama.com) for hosted indexes
68
+ - **12 themes** — dark and light follow the system, with a toggle that remembers your choice; your own CSS always wins
69
+ - **Rich Markdown** — GitHub Flavoured Markdown, admonitions, steps, `[[wikilinks]]`, [Mermaid](https://mermaid.js.org) and [D2](https://d2lang.com) diagrams, click-to-zoom images, embedded PDFs
70
+ - **First-class code blocks** — build-time [Shiki](https://shiki.style) highlighting in the theme's own colours, with titles, line numbers, diff markers and focus
71
+ - **MDX components** — `<Callout>`, `<Card>`, `<CodeBlockTabs>` and friends, with no imports to write
72
+ - **Editor integration** — one extension covers VS Code, Cursor, Antigravity and other VS Code-compatible editors, remote workspaces included
73
+
61
74
  ## View in your browser
62
75
 
63
76
  ```bash
64
77
  npx seemore # view the current folder in your browser, live, no setup
65
78
  ```
66
79
 
67
- Open a terminal in your folder of Markdown files, run `npx seemore`, and open the address it prints (`http://localhost:4040` by default). From there it's live: add, rename, retitle or delete a file and the site updates immediately, navigation included.
68
-
69
- With no folder given, seemore serves the folder you're standing in. Point it at a subfolder with `npx seemore docs` if that's where your files live.
80
+ Run it in your folder of Markdown files (Node.js 20 or newer) and open the address it prints `http://localhost:4040` by default. From there it's live: add, rename, retitle or delete a file and the site updates immediately. Point it at a subfolder with `npx seemore docs` if that's where your files live.
70
81
 
71
82
  ## View in your code editor
72
83
 
73
- Install seemore from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=arifszn.seemore-vscode) or [Open VSX](https://open-vsx.org/extension/arifszn/seemore-vscode) to get the same rendered site as a panel beside your editor. No terminal, no `npx`, no browser tab to manage. Open VSX also covers VS Code-compatible editors — Cursor, Antigravity, and others.
84
+ Install **seemore** from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=arifszn.seemore-vscode) or [Open VSX](https://open-vsx.org/extension/arifszn/seemore-vscode) to get the same rendered site as a panel beside your editor no terminal, no `npx`, no browser tab to manage. The extension bundles the CLI, so nothing is downloaded or put on your PATH. Open VSX also covers VS Code-compatible editors — Cursor, Antigravity, and others.
74
85
 
75
86
  1. Open any Markdown file.
76
- 2. Click the seemore icon in the editor's title bar, or right-click a folder in the explorer and choose **Open Folder in seemore**.
87
+ 2. Click the **seemore** icon in the editor's title bar, or right-click a folder in the explorer and choose **Open Folder in seemore**.
77
88
  3. The rendered site opens beside your editor, scoped to that file's folder.
78
89
 
79
90
  ### Commands
@@ -88,27 +99,37 @@ Install seemore from the [VS Code Marketplace](https://marketplace.visualstudio.
88
99
 
89
100
  | Setting | Default | Effect |
90
101
  | --- | --- | --- |
91
- | `seemore.path` | *(bundled copy)* | Path to a `seemore` CLI entry point to use instead of the version bundled with the extension. Leave empty unless you're developing seemore itself. |
102
+ | `seemore.path` | *(bundled copy)* | Path to a `seemore` CLI entry point to use instead of the version bundled with the extension. Leave empty unless you're developing **seemore** itself. |
92
103
 
93
- ## Publish it to the web
104
+ ## Edit your files from the browser
94
105
 
95
- ```bash
96
- npx seemore build # static export to dist/ for any host
106
+ The preview is not just for reading — it is the fastest way to fix what you are reading. Double-click any paragraph, heading, list item, quote or table cell and it opens in a small editor holding that block's **Markdown source**: `**bold**` stays `**bold**`, links stay links, tables stay tables. Fix the text and hit **Save** and the change is written to the file on disk. Nothing is written until you say so: clicking away leaves the editor open rather than saving behind your back. It works the same in the VS Code panel, which runs the same dev server.
107
+
108
+ Inline editing is for local previews only — `seemore build` output is static, so nothing is emitted there. It is on by default in dev; switch it off with the `!` prefix:
109
+
110
+ ```ts
111
+ export default defineConfig({
112
+ features: ['!content.edit'],
113
+ });
97
114
  ```
98
115
 
99
- The result is a `dist/` folder of plain web files: drop it on [Netlify](https://netlify.com), [Surge](https://surge.sh), [Cloudflare Pages](https://pages.cloudflare.com) or [GitHub Pages](https://pages.github.com), or hand it to any web host.
116
+ > [!NOTE]
117
+ > The dev server binds to localhost, so only your machine can reach the endpoint that writes. If you serve the site to your network with `--host`, anyone who can open the site can also edit your files — pass `features: ['!content.edit']` when you do that.
100
118
 
101
- **On GitHub Pages:** project sites live under `username.github.io/my-repo/`, not the root, so set `base` once:
119
+ ## Publish it to the web
102
120
 
103
- ```ts
104
- export default defineConfig({ base: '/my-repo/' });
121
+ ```bash
122
+ npx seemore build # static export to dist/ for any host
105
123
  ```
106
124
 
107
- (or `--base /my-repo/` on the CLI). Building under GitHub Actions without it set prints the exact line to add.
125
+ The result is a `dist/` folder of plain web files: drop it on [Netlify](https://netlify.com), [Surge](https://surge.sh), [Cloudflare Pages](https://pages.cloudflare.com) or [GitHub Pages](https://pages.github.com), or hand it to any web host. Every page is prerendered to its own `index.html`, next to a `404.html` that every static host honours. On top of that, the small conventions individual hosts look for — `_redirects` for Netlify and Cloudflare Pages, `200.html` for Surge, `.nojekyll` for GitHub Pages — are written for you.
126
+
127
+ > [!TIP]
128
+ > Project sites on GitHub Pages live under `username.github.io/my-repo/`, not the root, so set `base` once: `defineConfig({ base: '/my-repo/' })` (or `--base /my-repo/` on the CLI). Building under GitHub Actions without it set prints the exact line to add.
108
129
 
109
130
  ## Configuration
110
131
 
111
- Optional. A folder with no config file builds correctly in the browser, in your code editor, and when built for publishing. If you want to adjust things, create `seemore.config.ts` next to your content:
132
+ Optional a folder with no config file builds correctly everywhere. To adjust things, create `seemore.config.ts` next to your content:
112
133
 
113
134
  ```ts
114
135
  // seemore.config.ts
@@ -130,6 +151,10 @@ export default defineConfig({
130
151
  });
131
152
  ```
132
153
 
154
+ ### Search
155
+
156
+ The default `search: 'static'` needs no setup, no server and no account: the index is built from your Markdown and queried in the browser, in a Web Worker. `search.suggest` completes your query inline, and `search.highlight` carries the query onto the page you land on (`?h=…`), so search-result links are shareable. Hosted indexes are a drop-in swap whenever you want one: [Orama Cloud](https://orama.com) (`@orama/core`) or [Algolia](https://algolia.com) (`algoliasearch`), each needing nothing but its SDK installed.
157
+
133
158
  ### Themes
134
159
 
135
160
  Twelve built-in colour presets: `neutral` (default), `black`, `catppuccin`, `dusk`, `ocean`, `purple`, `ruby`, `solar`, `aspen`, `emerald`, `vitepress`, `shadcn`. For anything else, put your own CSS in `css`; it's appended last, so it wins. Dark and light follow your system setting, with a toggle that remembers your choice — `black` is built for dark mode, shown below with the toggle on.
@@ -160,7 +185,7 @@ Twelve built-in colour presets: `neutral` (default), `black`, `catppuccin`, `dus
160
185
 
161
186
  ### Features
162
187
 
163
- A flat list of switches for readers who want fine control, set as an array on the `features` key in `seemore.config.ts`. To turn on something that's off by default, add its flag name. To turn off something that's on by default, add its flag name prefixed with `!`.
188
+ A flat list of switches for fine control, set as an array on the `features` key. Turn something on by adding its flag name, turn something off with the `!` prefix; flags you don't mention keep their default.
164
189
 
165
190
  ```ts
166
191
  // seemore.config.ts
@@ -172,8 +197,6 @@ export default defineConfig({
172
197
  });
173
198
  ```
174
199
 
175
- Flags you don't mention are left at their default, so you only ever list the ones you're changing.
176
-
177
200
  | Flag | Default | Effect |
178
201
  | --- | --- | --- |
179
202
  | `navigation.instant.prefetch` | on | Load the target page on hover |
@@ -187,6 +210,7 @@ Flags you don't mention are left at their default, so you only ever list the one
187
210
  | `toc.integrate` | off | Merge the table of contents into the sidebar |
188
211
  | `content.code.copy` | on | Copy button on code blocks |
189
212
  | `content.action.edit` | on with `editLink` | Edit-this-page link |
213
+ | `content.edit` | on (dev only) | Double-click a block to edit its Markdown in place |
190
214
  | `content.image.zoom` | on | Click-to-zoom on content images |
191
215
  | `search.suggest` | on | Inline query completion |
192
216
  | `search.highlight` | on | Highlight the query on the page you land on |
@@ -202,8 +226,8 @@ Supports `.md` and `.mdx` both.
202
226
  - `[[wikilinks]]`, including `[[Page|label]]` and `[[Page#Heading]]`, the easiest way for you or your AI to link pages without relative paths to get right
203
227
  - Relative `.md` links are resolved to working links automatically
204
228
  - **[Mermaid](https://mermaid.js.org)** and **[D2](https://d2lang.com)** diagrams, both rendered live in the browser straight from a ` ```mermaid ` or ` ```d2 ` code fence
205
- - Sibling images inlined as hashed assets, sibling PDFs open in the browser's own viewer
206
- - Frontmatter (the `key: value` block at the top of a file) is validated, with errors that name the file and the field
229
+ - Sibling images inlined as hashed assets with click-to-zoom, sibling PDFs open in the browser's own viewer
230
+ - Frontmatter keys are validated
207
231
 
208
232
  ### Code blocks
209
233
 
@@ -230,7 +254,7 @@ The marker follows the language's own comment syntax, so `# [!code highlight]` i
230
254
 
231
255
  ### Components
232
256
 
233
- An `.mdx` file can use `<Callout>`, `<Card>`, `<Cards>`, `<CodeBlockTabs>`, `<Mermaid>`, `<D2>` and `<Pdf>` without importing anything. The set is deliberately small: Markdown has no imports, so every component is one seemore ships to every site whether it is used or not, and these are the ones that pair with something Markdown already expresses. Anything else — fumadocs' `<Tabs>`, `<Accordions>`, `<Files>` among them — fails the build, naming the file and the component. In a plain `.md` file a tag is not JSX at all: it is dropped and its text kept, so components need the `.mdx` extension. The [Content page](https://arifszn.github.io/seemore/content) has the details.
257
+ An `.mdx` file can use `<Callout>`, `<Card>`, `<Cards>`, `<CodeBlockTabs>`, `<Mermaid>`, `<D2>` and `<Pdf>` without importing anything. Anything else fails the build, naming the file and the component. In a plain `.md` file a tag is not JSX at all: it is dropped and its text kept, so components need the `.mdx` extension. The [Content page](https://arifszn.github.io/seemore/content) has the full syntax for each.
234
258
 
235
259
  Numbered headings — `## 1. Install it`, `## 2. Point it at a folder` — become a numbered sequence.
236
260
 
@@ -244,8 +268,6 @@ Numbered headings — `## 1. Install it`, `## 2. Point it at a folder` — becom
244
268
  | `guide/index.md` | `/guide` |
245
269
  | `guide/Deep Dive.md` | `/guide/deep-dive` |
246
270
 
247
- Both `/guide` and `/guide/` work on every host.
248
-
249
271
  ### Ordering
250
272
 
251
273
  Pages are ordered by:
package/dist/cli/index.js CHANGED
@@ -20,7 +20,7 @@ __export(paths_exports, {
20
20
  resolveContentRoot: () => resolveContentRoot
21
21
  });
22
22
  import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "fs";
23
- import { createHash } from "crypto";
23
+ import { createHash as createHash2 } from "crypto";
24
24
  import { tmpdir } from "os";
25
25
  import { dirname as dirname3, join as join2, resolve as resolve3 } from "path";
26
26
  import { fileURLToPath } from "url";
@@ -51,7 +51,7 @@ function packageDirOf(name, fromFile) {
51
51
  throw new Error(`seemore: could not locate the "${name}" package directory.`);
52
52
  }
53
53
  function cacheDir(contentRoot) {
54
- const key = createHash("sha256").update(resolve3(contentRoot)).digest("hex").slice(0, 12);
54
+ const key = createHash2("sha256").update(resolve3(contentRoot)).digest("hex").slice(0, 12);
55
55
  return join2(tmpdir(), "seemore", key);
56
56
  }
57
57
  function resolveContentRoot(cwd, explicit) {
@@ -75,7 +75,7 @@ import { parseArgs } from "util";
75
75
  import pc4 from "picocolors";
76
76
 
77
77
  // src/cli/build.ts
78
- import { mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
78
+ import { mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync5 } from "fs";
79
79
  import { createRequire as createRequire3 } from "module";
80
80
  import { tmpdir as tmpdir2 } from "os";
81
81
  import { isAbsolute as isAbsolute2, join as join8, relative as relative2, resolve as resolve5 } from "path";
@@ -124,6 +124,7 @@ var FEATURES = [
124
124
  "toc.integrate",
125
125
  "content.code.copy",
126
126
  "content.action.edit",
127
+ "content.edit",
127
128
  "content.image.zoom",
128
129
  "search.suggest",
129
130
  "search.highlight",
@@ -144,6 +145,10 @@ var FEATURE_DEFAULTS = {
144
145
  "content.code.copy": true,
145
146
  // Implicitly on when `editLink` is configured; there is nothing to link to otherwise.
146
147
  "content.action.edit": false,
148
+ // On by default, but only ever active in dev: the stamping that makes a block editable is
149
+ // not emitted by `seemore build`, and the endpoint that writes is registered only by the
150
+ // dev server. Switch it off with '!content.edit'.
151
+ "content.edit": true,
147
152
  "content.image.zoom": true,
148
153
  "search.suggest": true,
149
154
  "search.highlight": true,
@@ -515,6 +520,7 @@ import { dynamicLoader } from "fumadocs-core/source";
515
520
 
516
521
  // src/node/content/scan.ts
517
522
  import { readFileSync } from "fs";
523
+ import { createHash } from "crypto";
518
524
  import { basename, dirname as dirname2, join, resolve as resolve2 } from "path";
519
525
  import { globSync } from "tinyglobby";
520
526
  import { z as z4 } from "zod";
@@ -596,14 +602,17 @@ function scan(options) {
596
602
  for (const route of routes) {
597
603
  const absPath = join(contentRoot, route.file);
598
604
  let data;
605
+ let version;
599
606
  try {
600
- data = parseFrontmatter(readFileSync(absPath, "utf8"), route.file).data;
607
+ const text = readFileSync(absPath, "utf8");
608
+ data = parseFrontmatter(text, route.file).data;
609
+ version = createHash("sha256").update(text).digest("hex").slice(0, 12);
601
610
  } catch (error) {
602
611
  errors.push(error instanceof Error ? error.message : String(error));
603
612
  continue;
604
613
  }
605
614
  if (data.draft === true && options.includeDrafts !== true) continue;
606
- pages.push({ ...route, absPath, data: { ...data, title: titleFor(route, data, options.siteTitle) } });
615
+ pages.push({ ...route, absPath, version, data: { ...data, title: titleFor(route, data, options.siteTitle) } });
607
616
  }
608
617
  const files = pages.map((page) => ({
609
618
  type: "page",
@@ -961,6 +970,22 @@ function virtualPath(contentRoot, file) {
961
970
  return toPosix(relative(contentRoot, file.path));
962
971
  }
963
972
 
973
+ // src/node/vite/positions.ts
974
+ import { visit as visit2 } from "unist-util-visit";
975
+ var POSITION_ATTRIBUTE = "data-seemore-pos";
976
+ var EDITABLE = /* @__PURE__ */ new Set(["p", "h1", "h2", "h3", "h4", "h5", "h6", "li", "blockquote", "td", "th"]);
977
+ function rehypeSeemorePositions() {
978
+ return (tree) => {
979
+ visit2(tree, "element", (node) => {
980
+ if (!EDITABLE.has(node.tagName)) return;
981
+ const { start, end } = node.position ?? {};
982
+ if (start?.offset === void 0 || end?.offset === void 0) return;
983
+ node.properties ??= {};
984
+ node.properties[POSITION_ATTRIBUTE] = `${start.offset}:${end.offset}`;
985
+ });
986
+ };
987
+ }
988
+
964
989
  // src/node/vite/mdx.ts
965
990
  function createRemarkPlugins(options) {
966
991
  return [
@@ -993,18 +1018,21 @@ function createRemarkPlugins(options) {
993
1018
  () => remarkSeemoreLinks(options)
994
1019
  ];
995
1020
  }
996
- function createRehypePlugins() {
1021
+ function createRehypePlugins(options = {}) {
997
1022
  return [
998
1023
  // A fence in a language Shiki has no grammar for (anything an AI dreamt up) is plain code
999
1024
  // on the page, not a dead one: `plaintext` is special-cased by Shiki and never needs
1000
1025
  // loading.
1001
1026
  [rehypeCode, { fallbackLanguage: "plaintext" }],
1002
- rehypeToc
1027
+ rehypeToc,
1028
+ // After `rehype-code`, so a fence Shiki rebuilt is passed over rather than stamped with
1029
+ // the position of whatever it replaced.
1030
+ ...options.positions === true ? [rehypeSeemorePositions] : []
1003
1031
  ];
1004
1032
  }
1005
1033
 
1006
1034
  // src/node/vite/plugin.ts
1007
- import { readFileSync as readFileSync4 } from "fs";
1035
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1008
1036
  import { createRequire } from "module";
1009
1037
  import { dirname as dirname6 } from "path";
1010
1038
 
@@ -1062,6 +1090,33 @@ function formatBytes(bytes) {
1062
1090
 
1063
1091
  // src/node/vite/plugin.ts
1064
1092
  init_paths();
1093
+
1094
+ // src/node/content/edit.ts
1095
+ function spliceSource(content, request) {
1096
+ const { start, end, expected, text } = request;
1097
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > content.length) {
1098
+ return { ok: false, status: 400, error: "The edited range is not inside this file." };
1099
+ }
1100
+ if (content.slice(start, end) !== expected) {
1101
+ return {
1102
+ ok: false,
1103
+ status: 409,
1104
+ error: "This file changed since the page was rendered. Reload and try the edit again."
1105
+ };
1106
+ }
1107
+ return { ok: true, content: content.slice(0, start) + withEol(text, dominantEol(content)) + content.slice(end) };
1108
+ }
1109
+ function dominantEol(content) {
1110
+ const crlf = content.match(/\r\n/g)?.length ?? 0;
1111
+ const lf = (content.match(/\n/g)?.length ?? 0) - crlf;
1112
+ return crlf > lf ? "\r\n" : "\n";
1113
+ }
1114
+ function withEol(text, eol) {
1115
+ const normalised = text.replace(/\r\n/g, "\n");
1116
+ return eol === "\n" ? normalised : normalised.replace(/\n/g, "\r\n");
1117
+ }
1118
+
1119
+ // src/node/vite/plugin.ts
1065
1120
  var VIRTUAL = {
1066
1121
  tree: "virtual:seemore/tree",
1067
1122
  routes: "virtual:seemore/routes",
@@ -1161,6 +1216,13 @@ function seemorePlugin({ ctx, serveSearch = false }) {
1161
1216
  res.setHeader("Content-Type", "application/json");
1162
1217
  res.end(JSON.stringify({ url: withBase(ctx.config.base, page.url) }));
1163
1218
  });
1219
+ if (ctx.config.features["content.edit"]) {
1220
+ devServer.middlewares.use((req, res, next) => {
1221
+ const path = (req.url ?? "").split("?")[0] ?? "";
1222
+ if (path !== SOURCE_ENDPOINT && path !== withBase(ctx.config.base, SOURCE_ENDPOINT)) return next();
1223
+ void handleSource(ctx, req, res).catch(next);
1224
+ });
1225
+ }
1164
1226
  },
1165
1227
  /** Called by the watcher after a rescan. */
1166
1228
  api: {
@@ -1175,6 +1237,63 @@ function seemorePlugin({ ctx, serveSearch = false }) {
1175
1237
  }
1176
1238
  };
1177
1239
  }
1240
+ var SOURCE_ENDPOINT = "/__seemore/source";
1241
+ async function handleSource(ctx, req, res) {
1242
+ if (req.method === "GET") return handleSourceRead(ctx, req, res);
1243
+ if (req.method === "PUT") return handleSourceWrite(ctx, req, res);
1244
+ res.setHeader("Allow", "GET, PUT");
1245
+ return send(res, 405, { error: `${req.method ?? "This method"} is not allowed here.` });
1246
+ }
1247
+ function handleSourceRead(ctx, req, res) {
1248
+ const query = new URLSearchParams((req.url ?? "").split("?")[1] ?? "");
1249
+ const page = resolvePage(ctx, query.get("file"));
1250
+ if (page === void 0) return send(res, 404, { error: "That file is not part of this site." });
1251
+ const start = Number(query.get("start"));
1252
+ const end = Number(query.get("end"));
1253
+ const content = readFileSync4(page.absPath, "utf8");
1254
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > content.length) {
1255
+ return send(res, 400, { error: "The requested range is not inside this file." });
1256
+ }
1257
+ return send(res, 200, { text: content.slice(start, end) });
1258
+ }
1259
+ async function handleSourceWrite(ctx, req, res) {
1260
+ let body;
1261
+ try {
1262
+ body = JSON.parse(await readBody(req));
1263
+ } catch {
1264
+ return send(res, 400, { error: "The request body was not valid JSON." });
1265
+ }
1266
+ const page = resolvePage(ctx, body.file);
1267
+ if (page === void 0) return send(res, 404, { error: "That file is not part of this site." });
1268
+ if (typeof body.expected !== "string" || typeof body.text !== "string") {
1269
+ return send(res, 400, { error: "Both `expected` and `text` are required." });
1270
+ }
1271
+ const content = readFileSync4(page.absPath, "utf8");
1272
+ const result = spliceSource(content, {
1273
+ start: body.start,
1274
+ end: body.end,
1275
+ expected: body.expected,
1276
+ text: body.text
1277
+ });
1278
+ if (!result.ok) return send(res, result.status, { error: result.error });
1279
+ writeFileSync3(page.absPath, result.content, "utf8");
1280
+ return send(res, 200, { ok: true });
1281
+ }
1282
+ function resolvePage(ctx, file) {
1283
+ if (typeof file !== "string" || file === "") return void 0;
1284
+ const absFile = canonicalise(file);
1285
+ return ctx.pages().find((page) => page.absPath === absFile);
1286
+ }
1287
+ async function readBody(req) {
1288
+ const chunks = [];
1289
+ for await (const chunk of req) chunks.push(chunk);
1290
+ return Buffer.concat(chunks).toString("utf8");
1291
+ }
1292
+ function send(res, status, payload) {
1293
+ res.statusCode = status;
1294
+ res.setHeader("Content-Type", "application/json");
1295
+ res.end(JSON.stringify(payload));
1296
+ }
1178
1297
  function hotStoreModule(suffix, value) {
1179
1298
  return `const state = import.meta.hot
1180
1299
  ? (import.meta.hot.data.seemore${suffix} ||= { listeners: new Set() })
@@ -1207,6 +1326,7 @@ function renderRoutesValue(ctx) {
1207
1326
  ` url: ${json(page.url)},`,
1208
1327
  ` file: ${json(page.file)},`,
1209
1328
  ` absPath: ${json(page.absPath)},`,
1329
+ ` version: ${json(page.version)},`,
1210
1330
  ` title: ${json(page.data.title)},`,
1211
1331
  ` description: ${json(page.data.description ?? null)},`,
1212
1332
  ` load: () => import(${json(specifier)}),`,
@@ -1336,7 +1456,7 @@ function createViteConfig({ ctx, mode, outDir, ssrOutDir }) {
1336
1456
  getResolver: () => ctx.resolver(),
1337
1457
  onWarning: (message) => ctx.warnings.add(message)
1338
1458
  }),
1339
- rehypePlugins: createRehypePlugins(),
1459
+ rehypePlugins: createRehypePlugins({ positions: mode === "dev" && ctx.config.features["content.edit"] }),
1340
1460
  // MDX compiles its own JSX. Vite's builtin transform infers a file's language from its
1341
1461
  // extension and does not know `.md`/`.mdx`, so leaving JSX in the output would fail to
1342
1462
  // parse. Fast Refresh is unaffected: it is a separate transform, applied to these files
@@ -1459,7 +1579,7 @@ async function loadPrerenderModule(ctx, ssrOutDir) {
1459
1579
  }
1460
1580
 
1461
1581
  // src/node/social/cards.ts
1462
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
1582
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
1463
1583
  import { dirname as dirname7, join as join7 } from "path";
1464
1584
 
1465
1585
  // src/shared/og.ts
@@ -1485,7 +1605,7 @@ async function generateSocialCards(ctx, outDir) {
1485
1605
  if (png === void 0) continue;
1486
1606
  const target = join7(outDir, ogImagePath(page.url));
1487
1607
  mkdirSync2(dirname7(target), { recursive: true });
1488
- writeFileSync3(target, png);
1608
+ writeFileSync4(target, png);
1489
1609
  written++;
1490
1610
  }
1491
1611
  return written;
@@ -1553,7 +1673,7 @@ async function runBuild(options) {
1553
1673
  if (config.search.provider === "static") {
1554
1674
  const index = await buildSearchIndex(ctx);
1555
1675
  mkdirSync3(join8(outDir, "api"), { recursive: true });
1556
- writeFileSync4(join8(outDir, "api", "search.json"), index, "utf8");
1676
+ writeFileSync5(join8(outDir, "api", "search.json"), index, "utf8");
1557
1677
  const size = measureIndex(index);
1558
1678
  console.log(pc2.dim(`seemore search index ${formatBytes(size.gzipped)} gzipped`));
1559
1679
  if (size.warning !== void 0) ctx.warnings.add(size.warning);