mkdnsite 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Goode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # mkdnsite
2
+
3
+ **Markdown for the web. HTML for humans, Markdown for agents.**
4
+
5
+ mkdnsite turns a directory of Markdown files into a live website — with zero build step. The same URL serves beautifully rendered HTML to browsers and clean Markdown to AI agents, using standard HTTP content negotiation.
6
+
7
+ ```
8
+ Cloudflare, Vercel, et al: HTML → Markdown (for AI)
9
+ mkdnsite: Markdown → HTML (for humans)
10
+
11
+ Same content negotiation standard. Opposite direction.
12
+ Markdown is the source of truth.
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ bun add -g mkdnsite
19
+ mkdir my-site && echo "# Hello World" > my-site/index.md
20
+ mkdnsite ./my-site
21
+ ```
22
+
23
+ ```bash
24
+ curl http://localhost:3000 # HTML for humans
25
+ curl -H "Accept: text/markdown" http://localhost:3000 # Markdown for agents
26
+ curl http://localhost:3000/llms.txt # AI content index
27
+ ```
28
+
29
+ Also runs on Node and Deno:
30
+
31
+ ```bash
32
+ node src/cli.ts ./my-site
33
+ deno run --allow-read --allow-net src/cli.ts ./my-site
34
+ ```
35
+
36
+ ## Features
37
+
38
+ - **Zero build step** — runtime Markdown→HTML via React SSR
39
+ - **Content negotiation** — `Accept: text/markdown` returns raw MD
40
+ - **Beautiful by default** — shadcn/Radix-inspired typography with light/dark mode
41
+ - **Theme toggle** — sun/moon button with animated circular reveal (View Transitions API)
42
+ - **Syntax highlighting** — Shiki SSR with dual-theme support (light + dark)
43
+ - **Math rendering** — KaTeX via `remark-math` + `rehype-katex` (SSR)
44
+ - **GFM Alerts** — `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]`
45
+ - **GitHub-Flavored Markdown** — tables, task lists, strikethrough, autolinks
46
+ - **Collapsible sections** — `<details>` and `<summary>` with HTML sanitization
47
+ - **Mermaid diagrams** — rendered client-side from fenced code blocks
48
+ - **Copy-to-clipboard** — on all code blocks
49
+ - **Pluggable rendering** — custom React components per element
50
+ - **Auto `/llms.txt`** — AI agents discover your content
51
+ - **`x-markdown-tokens` header** — Cloudflare-compatible token count
52
+ - **`Content-Signal` header** — AI consent signaling
53
+ - **YAML frontmatter** — title, description, ordering, tags, drafts
54
+ - **Filesystem routing** — directory structure = URL structure
55
+ - **Auto-navigation** — sidebar from file tree
56
+ - **Portable** — runs on Bun, Node 22+, and Deno
57
+
58
+ ## Architecture
59
+
60
+ ```
61
+ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐
62
+ │ Request │────▶│ Handler │────▶│ Response │
63
+ │ Accept: ??? │ │ (portable) │ │ HTML or MD │
64
+ └─────────────┘ └──────┬───────┘ └───────────────┘
65
+
66
+ ┌────────────┼────────────┐
67
+ ▼ ▼ ▼
68
+ ┌──────────┐ ┌──────────┐ ┌──────────┐
69
+ │ Content │ │ Renderer │ │ Negotiate│
70
+ │ Source │ │ (React) │ │ │
71
+ └──────────┘ └──────────┘ └──────────┘
72
+ │ │
73
+ ┌────────┼──┐ ┌────┴────┐
74
+ ▼ ▼ ▼ ▼ ▼
75
+ Local GitHub R2 portable bun-native
76
+ FS API (react-md) (Bun.md)
77
+ ```
78
+
79
+ ### Deployment Adapters
80
+
81
+ mkdnsite ships adapter stubs for multiple platforms:
82
+
83
+ | Adapter | Status | Content Source |
84
+ |---------|--------|----------------|
85
+ | Local (Bun/Node/Deno) | Working | Filesystem |
86
+ | Cloudflare Workers | Stub | R2 / GitHub |
87
+ | Vercel Edge | Stub | Blob / GitHub |
88
+ | Netlify | Stub | TBD |
89
+ | Fly.io | Stub | Filesystem (volumes) |
90
+
91
+ ### Theme Modes
92
+
93
+ - **prose** (default): Beautiful typography via CSS, zero-config
94
+ - **components**: Full React component overrides with your own styling
95
+
96
+ ## Configuration
97
+
98
+ Create `mkdnsite.config.ts`:
99
+
100
+ ```typescript
101
+ import type { MkdnSiteConfig } from 'mkdnsite'
102
+
103
+ export default {
104
+ contentDir: './docs',
105
+ site: {
106
+ title: 'My Documentation',
107
+ url: 'https://docs.example.com'
108
+ },
109
+ theme: {
110
+ mode: 'prose',
111
+ showNav: true,
112
+ colorScheme: 'system',
113
+ syntaxTheme: 'github-light',
114
+ syntaxThemeDark: 'github-dark'
115
+ },
116
+ client: {
117
+ enabled: true,
118
+ themeToggle: true,
119
+ math: true,
120
+ mermaid: true,
121
+ copyButton: true
122
+ },
123
+ renderer: 'portable'
124
+ } satisfies Partial<MkdnSiteConfig>
125
+ ```
126
+
127
+ Or use CLI flags:
128
+
129
+ ```bash
130
+ mkdnsite ./docs --port 8080 --title "My Docs"
131
+ mkdnsite ./docs --color-scheme dark --no-theme-toggle
132
+ mkdnsite ./docs --no-math --no-client-js
133
+ mkdnsite ./docs --renderer bun-native
134
+ ```
135
+
136
+ ### CLI Options
137
+
138
+ | Flag | Description | Default |
139
+ |------|-------------|---------|
140
+ | `-p, --port <n>` | Port to listen on | `3000` |
141
+ | `--title <text>` | Site title | `mkdnsite` |
142
+ | `--url <url>` | Base URL for absolute links | — |
143
+ | `--static <dir>` | Static assets directory | — |
144
+ | `--color-scheme <val>` | `system`, `light`, or `dark` | `system` |
145
+ | `--theme-mode <mode>` | `prose` or `components` | `prose` |
146
+ | `--renderer <engine>` | `portable` or `bun-native` | `portable` |
147
+ | `--no-nav` | Disable navigation sidebar | — |
148
+ | `--no-llms-txt` | Disable /llms.txt generation | — |
149
+ | `--no-negotiate` | Disable content negotiation | — |
150
+ | `--no-client-js` | Disable all client-side JavaScript | — |
151
+ | `--no-theme-toggle` | Disable light/dark theme toggle | — |
152
+ | `--no-math` | Disable KaTeX math rendering | — |
153
+
154
+ ## Content Negotiation
155
+
156
+ Same pattern as Cloudflare Markdown for Agents and Vercel:
157
+
158
+ | Client | Accept Header | Response |
159
+ |--------|--------------|----------|
160
+ | Browser | `text/html` | React SSR rendered page |
161
+ | Claude Code | `text/markdown, text/html, */*` | Raw Markdown |
162
+ | Cursor | `text/markdown;q=1.0, text/html;q=0.7` | Raw Markdown |
163
+ | curl (default) | `*/*` | Rendered HTML |
164
+ | `.md` URL suffix | — | Raw Markdown |
165
+
166
+ ## Programmatic Usage
167
+
168
+ ```typescript
169
+ import { createHandler, resolveConfig, FilesystemSource } from 'mkdnsite'
170
+ import { createRenderer } from 'mkdnsite'
171
+
172
+ const config = resolveConfig({ site: { title: 'My Site' } })
173
+ const renderer = await createRenderer({
174
+ syntaxTheme: 'github-light',
175
+ syntaxThemeDark: 'github-dark'
176
+ })
177
+ const handler = createHandler({
178
+ source: new FilesystemSource('./content'),
179
+ renderer,
180
+ config
181
+ })
182
+
183
+ // Use with any platform's serve API
184
+ Bun.serve({ fetch: handler, port: 3000 })
185
+ ```
186
+
187
+ ## Roadmap
188
+
189
+ - [x] Syntax highlighting via Shiki (server-side)
190
+ - [x] Light/dark theme toggle with animation
191
+ - [x] KaTeX math rendering (SSR)
192
+ - [x] GFM alerts (NOTE, TIP, IMPORTANT, WARNING, CAUTION)
193
+ - [x] HTML sanitization for safe raw HTML passthrough
194
+ - [ ] Table of contents per page
195
+ - [ ] Client-side search with pre-built index
196
+ - [ ] GitHub content source (serve from repo)
197
+ - [ ] Custom themes (CSS files or npm packages)
198
+ - [ ] Hosted service at mkdn.io
199
+ - [ ] RSS feed generation
200
+ - [ ] OpenGraph meta tags
201
+ - [ ] Human vs. AI traffic analytics
202
+
203
+ ## Links
204
+
205
+ - **Documentation**: [mkdn.site](https://mkdn.site)
206
+ - **Hosted service**: [mkdn.io](https://mkdn.io) (coming soon)
207
+ - **Repository**: [github.com/mkdnsite/mkdnsite](https://github.com/mkdnsite/mkdnsite)
208
+
209
+ ## License
210
+
211
+ MIT
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "mkdnsite",
3
+ "version": "0.0.1",
4
+ "description": "Markdown for the web. HTML for humans, Markdown for agents.",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "bin": {
8
+ "mkdnsite": "src/cli.ts"
9
+ },
10
+ "exports": {
11
+ ".": "./src/index.ts",
12
+ "./adapters/cloudflare": "./src/adapters/cloudflare.ts",
13
+ "./adapters/vercel": "./src/adapters/vercel.ts",
14
+ "./adapters/netlify": "./src/adapters/netlify.ts",
15
+ "./adapters/fly": "./src/adapters/fly.ts"
16
+ },
17
+ "scripts": {
18
+ "dev": "bun run --watch src/cli.ts ./content",
19
+ "start": "bun run src/cli.ts",
20
+ "test": "bun test",
21
+ "lint": "ts-standard src/ test/",
22
+ "lint:fix": "ts-standard src/ test/ --fix",
23
+ "tsc": "tsc --noEmit"
24
+ },
25
+ "keywords": [
26
+ "markdown",
27
+ "server",
28
+ "content-negotiation",
29
+ "ai-agents",
30
+ "llms-txt",
31
+ "gfm",
32
+ "react",
33
+ "ssr",
34
+ "tailwind",
35
+ "bun"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/mkdnsite/mkdnsite.git"
40
+ },
41
+ "homepage": "https://mkdn.site",
42
+ "license": "MIT",
43
+ "files": [
44
+ "src"
45
+ ],
46
+ "dependencies": {
47
+ "gray-matter": "^4.0.3",
48
+ "katex": "^0.16.38",
49
+ "lucide-react": "^0.577.0",
50
+ "react": "^19.0.0",
51
+ "react-dom": "^19.0.0",
52
+ "react-markdown": "^9.0.0",
53
+ "rehype-katex": "^7.0.1",
54
+ "rehype-raw": "^7.0.0",
55
+ "rehype-sanitize": "^6.0.0",
56
+ "rehype-slug": "^6.0.0",
57
+ "remark-gfm": "^4.0.0",
58
+ "remark-github-blockquote-alert": "^2.0.1",
59
+ "remark-math": "^6.0.0",
60
+ "shiki": "^3.0.0"
61
+ },
62
+ "ts-standard": {
63
+ "project": "./tsconfig.json"
64
+ },
65
+ "devDependencies": {
66
+ "@types/bun": "latest",
67
+ "@types/react": "^19.0.0",
68
+ "@types/react-dom": "^19.0.0",
69
+ "ts-standard": "^12.0.2",
70
+ "typescript": "^5.7.0"
71
+ }
72
+ }
@@ -0,0 +1,85 @@
1
+ import type { DeploymentAdapter } from './types.ts'
2
+ import type { MkdnSiteConfig } from '../config/schema.ts'
3
+ import type { ContentSource } from '../content/types.ts'
4
+ import type { MarkdownRenderer } from '../render/types.ts'
5
+ import { createRenderer } from '../render/types.ts'
6
+
7
+ /**
8
+ * Cloudflare Workers deployment adapter.
9
+ *
10
+ * Uses R2 for content storage, KV for caching.
11
+ * Wildcard DNS routes (*.mkdn.io) for hosted service.
12
+ *
13
+ * Usage in a Worker:
14
+ *
15
+ * ```ts
16
+ * import { createHandler, resolveConfig } from 'mkdnsite'
17
+ * import { CloudflareAdapter } from 'mkdnsite/adapters/cloudflare'
18
+ *
19
+ * export default {
20
+ * fetch (request: Request, env: Env): Promise<Response> {
21
+ * const adapter = new CloudflareAdapter(env)
22
+ * const config = resolveConfig({ site: { title: env.SITE_TITLE } })
23
+ * const handler = createHandler({
24
+ * source: adapter.createContentSource(config),
25
+ * renderer: adapter.createRenderer(config),
26
+ * config
27
+ * })
28
+ * return handler(request)
29
+ * }
30
+ * }
31
+ * ```
32
+ */
33
+ export class CloudflareAdapter implements DeploymentAdapter {
34
+ readonly name = 'cloudflare-workers'
35
+ private readonly env: CloudflareEnv
36
+
37
+ constructor (env: CloudflareEnv) {
38
+ this.env = env
39
+ }
40
+
41
+ createContentSource (_config: MkdnSiteConfig): ContentSource {
42
+ // TODO: Implement R2ContentSource
43
+ // return new R2ContentSource(this.env.CONTENT_BUCKET)
44
+ throw new Error(
45
+ 'CloudflareAdapter.createContentSource() not yet implemented. ' +
46
+ 'Provide an R2-backed ContentSource implementation.'
47
+ )
48
+ }
49
+
50
+ async createRenderer (_config: MkdnSiteConfig): Promise<MarkdownRenderer> {
51
+ // CF Workers don't have Bun.markdown, always use portable
52
+ return await createRenderer('portable')
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Expected Cloudflare Worker environment bindings.
58
+ */
59
+ export interface CloudflareEnv {
60
+ /** R2 bucket for markdown content */
61
+ CONTENT_BUCKET?: R2Bucket
62
+ /** KV namespace for caching */
63
+ CACHE_KV?: KVNamespace
64
+ /** Site title from env var */
65
+ SITE_TITLE?: string
66
+ /** Site URL from env var */
67
+ SITE_URL?: string
68
+ }
69
+
70
+ // Type stubs for CF runtime types (not available in non-CF environments)
71
+ interface R2Bucket {
72
+ get: (key: string) => Promise<R2Object | null>
73
+ list: (options?: Record<string, unknown>) => Promise<{ objects: R2Object[] }>
74
+ }
75
+
76
+ interface R2Object {
77
+ key: string
78
+ uploaded: Date
79
+ text: () => Promise<string>
80
+ }
81
+
82
+ interface KVNamespace {
83
+ get: (key: string) => Promise<string | null>
84
+ put: (key: string, value: string, options?: Record<string, unknown>) => Promise<void>
85
+ }
@@ -0,0 +1,22 @@
1
+ import type { DeploymentAdapter } from './types.ts'
2
+ import type { MkdnSiteConfig } from '../config/schema.ts'
3
+ import type { ContentSource } from '../content/types.ts'
4
+ import type { MarkdownRenderer } from '../render/types.ts'
5
+ import { FilesystemSource } from '../content/filesystem.ts'
6
+ import { createRenderer } from '../render/types.ts'
7
+
8
+ /**
9
+ * Fly.io adapter.
10
+ * Uses filesystem source (persistent volumes on Fly).
11
+ */
12
+ export class FlyAdapter implements DeploymentAdapter {
13
+ readonly name = 'fly'
14
+
15
+ createContentSource (config: MkdnSiteConfig): ContentSource {
16
+ return new FilesystemSource(config.contentDir)
17
+ }
18
+
19
+ async createRenderer (_config: MkdnSiteConfig): Promise<MarkdownRenderer> {
20
+ return await createRenderer('portable')
21
+ }
22
+ }
@@ -0,0 +1,153 @@
1
+ import type { DeploymentAdapter } from './types.ts'
2
+ import { detectRuntime } from './types.ts'
3
+ import type { MkdnSiteConfig } from '../config/schema.ts'
4
+ import type { ContentSource } from '../content/types.ts'
5
+ import type { MarkdownRenderer } from '../render/types.ts'
6
+ import { FilesystemSource } from '../content/filesystem.ts'
7
+ import { createRenderer } from '../render/types.ts'
8
+
9
+ export class LocalAdapter implements DeploymentAdapter {
10
+ readonly name: string
11
+ private rendererEngine: string = 'portable'
12
+
13
+ constructor () {
14
+ this.name = `local (${detectRuntime()})`
15
+ }
16
+
17
+ createContentSource (config: MkdnSiteConfig): ContentSource {
18
+ return new FilesystemSource(config.contentDir)
19
+ }
20
+
21
+ async createRenderer (config: MkdnSiteConfig): Promise<MarkdownRenderer> {
22
+ const renderer = await createRenderer({
23
+ engine: config.renderer,
24
+ syntaxTheme: config.theme.syntaxTheme,
25
+ syntaxThemeDark: config.theme.syntaxThemeDark,
26
+ math: config.client.math
27
+ })
28
+ this.rendererEngine = renderer.engine
29
+ return renderer
30
+ }
31
+
32
+ async start (
33
+ handler: (request: Request) => Promise<Response>,
34
+ config: MkdnSiteConfig
35
+ ): Promise<() => void> {
36
+ const runtime = detectRuntime()
37
+
38
+ if (runtime === 'bun') {
39
+ return this.startBun(handler, config)
40
+ }
41
+
42
+ if (runtime === 'deno') {
43
+ return this.startDeno(handler, config)
44
+ }
45
+
46
+ return await this.startNode(handler, config)
47
+ }
48
+
49
+ private startBun (
50
+ handler: (request: Request) => Promise<Response>,
51
+ config: MkdnSiteConfig
52
+ ): () => void {
53
+ const server = Bun.serve({
54
+ port: config.server.port,
55
+ hostname: config.server.hostname,
56
+ fetch: handler
57
+ })
58
+
59
+ this.printStartup(config, server.port ?? config.server.port)
60
+ return () => { void server.stop() }
61
+ }
62
+
63
+ private startDeno (
64
+ handler: (request: Request) => Promise<Response>,
65
+ config: MkdnSiteConfig
66
+ ): () => void {
67
+ const { port, hostname } = config.server
68
+
69
+ // Deno.serve is available globally in Deno
70
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
+ const DenoNs = (globalThis as any).Deno
72
+ const server = DenoNs.serve({ port, hostname }, handler)
73
+
74
+ this.printStartup(config, port)
75
+ return () => { void server.shutdown() }
76
+ }
77
+
78
+ private async startNode (
79
+ handler: (request: Request) => Promise<Response>,
80
+ config: MkdnSiteConfig
81
+ ): Promise<() => void> {
82
+ const { port, hostname } = config.server
83
+ const { createServer } = await import('node:http')
84
+
85
+ const server = createServer((req, res) => {
86
+ const host = req.headers.host ?? `${hostname}:${String(port)}`
87
+ const url = new URL(req.url ?? '/', `http://${host}`)
88
+
89
+ const headers = new Headers()
90
+ for (const [key, value] of Object.entries(req.headers)) {
91
+ if (value != null) {
92
+ if (Array.isArray(value)) {
93
+ for (const v of value) headers.append(key, v)
94
+ } else {
95
+ headers.set(key, value)
96
+ }
97
+ }
98
+ }
99
+
100
+ const request = new Request(url.toString(), {
101
+ method: req.method,
102
+ headers
103
+ })
104
+
105
+ handler(request)
106
+ .then(async (response) => {
107
+ const resHeaders: Record<string, string> = {}
108
+ response.headers.forEach((value, key) => {
109
+ resHeaders[key] = value
110
+ })
111
+ res.writeHead(response.status, resHeaders)
112
+ const body = Buffer.from(await response.arrayBuffer())
113
+ res.end(body)
114
+ })
115
+ .catch((err) => {
116
+ console.error('mkdnsite: request error', err)
117
+ res.writeHead(500)
118
+ res.end('Internal Server Error')
119
+ })
120
+ })
121
+
122
+ await new Promise<void>((resolve) => {
123
+ server.listen(port, hostname, () => { resolve() })
124
+ })
125
+
126
+ this.printStartup(config, port)
127
+ return () => { server.close() }
128
+ }
129
+
130
+ private printStartup (config: MkdnSiteConfig, port: number): void {
131
+ const url = `http://localhost:${String(port)}`
132
+ console.log('')
133
+ console.log(' ┌──────────────────────────────────────────────┐')
134
+ console.log(' │ │')
135
+ console.log(' │ mkdnsite is running │')
136
+ console.log(` │ ${url.padEnd(42)} │`)
137
+ console.log(' │ │')
138
+ console.log(` │ Runtime: ${this.name.padEnd(32)} │`)
139
+ console.log(` │ Content: ${config.contentDir.padEnd(32)} │`)
140
+ console.log(` │ Renderer: ${this.rendererEngine.padEnd(30)} │`)
141
+ console.log(` │ Theme mode: ${config.theme.mode.padEnd(28)} │`)
142
+ console.log(` │ Client JS: ${(config.client.enabled ? 'on' : 'off').padEnd(29)} │`)
143
+ console.log(` │ Content negotiation: ${(config.negotiation.enabled ? 'on' : 'off').padEnd(19)} │`)
144
+ console.log(' │ │')
145
+ console.log(' └──────────────────────────────────────────────┘')
146
+ console.log('')
147
+ console.log(' Try:')
148
+ console.log(` curl ${url}`)
149
+ console.log(` curl -H "Accept: text/markdown" ${url}`)
150
+ console.log(` curl ${url}/llms.txt`)
151
+ console.log('')
152
+ }
153
+ }
@@ -0,0 +1,17 @@
1
+ import type { DeploymentAdapter } from './types.ts'
2
+ import type { MkdnSiteConfig } from '../config/schema.ts'
3
+ import type { ContentSource } from '../content/types.ts'
4
+ import type { MarkdownRenderer } from '../render/types.ts'
5
+ import { createRenderer } from '../render/types.ts'
6
+
7
+ export class NetlifyAdapter implements DeploymentAdapter {
8
+ readonly name = 'netlify'
9
+
10
+ createContentSource (_config: MkdnSiteConfig): ContentSource {
11
+ throw new Error('NetlifyAdapter.createContentSource() not yet implemented.')
12
+ }
13
+
14
+ async createRenderer (_config: MkdnSiteConfig): Promise<MarkdownRenderer> {
15
+ return await createRenderer('portable')
16
+ }
17
+ }
@@ -0,0 +1,54 @@
1
+ import type { MkdnSiteConfig } from '../config/schema.ts'
2
+ import type { ContentSource } from '../content/types.ts'
3
+ import type { MarkdownRenderer } from '../render/types.ts'
4
+
5
+ /**
6
+ * Deployment adapter interface.
7
+ *
8
+ * Each target deployment environment (Bun local, CF Workers,
9
+ * Vercel Edge, Netlify, Fly.io, etc.) implements this interface
10
+ * to provide the appropriate content source, renderer, and
11
+ * any environment-specific setup.
12
+ *
13
+ * The adapter is responsible for:
14
+ * 1. Creating the appropriate ContentSource for its environment
15
+ * 2. Creating the appropriate MarkdownRenderer
16
+ * 3. Providing the fetch handler in the format the platform expects
17
+ * 4. Any platform-specific initialization (e.g. binding to KV, R2, etc.)
18
+ */
19
+ export interface DeploymentAdapter {
20
+ /** Human-readable name for logging */
21
+ readonly name: string
22
+
23
+ /** Create the content source for this environment */
24
+ createContentSource: (config: MkdnSiteConfig) => ContentSource
25
+
26
+ /** Create the markdown renderer for this environment */
27
+ createRenderer: (config: MkdnSiteConfig) => Promise<MarkdownRenderer>
28
+
29
+ /**
30
+ * Start the server (for adapters that manage their own server).
31
+ * Returns a cleanup function.
32
+ * For serverless adapters, this is a no-op.
33
+ */
34
+ start?: (
35
+ handler: (request: Request) => Promise<Response>,
36
+ config: MkdnSiteConfig
37
+ ) => Promise<(() => void) | undefined>
38
+ }
39
+
40
+ /**
41
+ * Detect the current runtime environment.
42
+ */
43
+ export function detectRuntime (): 'bun' | 'cloudflare' | 'vercel' | 'netlify' | 'node' | 'deno' {
44
+ if (typeof Bun !== 'undefined') return 'bun'
45
+ if (typeof globalThis !== 'undefined') {
46
+ const g = globalThis as Record<string, unknown>
47
+ if (g.caches != null && g.MINIFLARE != null) return 'cloudflare'
48
+ if (g.Netlify != null) return 'netlify'
49
+ if (g.Deno != null) return 'deno'
50
+ }
51
+ // Check for Vercel Edge Runtime
52
+ if (process?.env?.VERCEL != null) return 'vercel'
53
+ return 'node'
54
+ }
@@ -0,0 +1,48 @@
1
+ import type { DeploymentAdapter } from './types.ts'
2
+ import type { MkdnSiteConfig } from '../config/schema.ts'
3
+ import type { ContentSource } from '../content/types.ts'
4
+ import type { MarkdownRenderer } from '../render/types.ts'
5
+ import { createRenderer } from '../render/types.ts'
6
+
7
+ /**
8
+ * Vercel Edge/Serverless deployment adapter.
9
+ *
10
+ * For Vercel, the handler is exported as a standard Edge Function.
11
+ * Content can come from Vercel Blob Storage or a connected GitHub repo.
12
+ *
13
+ * Usage in a Vercel Edge Function:
14
+ *
15
+ * ```ts
16
+ * // app/api/[...path]/route.ts
17
+ * import { createHandler, resolveConfig } from 'mkdnsite'
18
+ * import { VercelAdapter } from 'mkdnsite/adapters/vercel'
19
+ *
20
+ * const adapter = new VercelAdapter()
21
+ * const config = resolveConfig({
22
+ * site: { title: 'My Docs' }
23
+ * })
24
+ *
25
+ * export const GET = createHandler({
26
+ * source: adapter.createContentSource(config),
27
+ * renderer: adapter.createRenderer(config),
28
+ * config
29
+ * })
30
+ *
31
+ * export const runtime = 'edge'
32
+ * ```
33
+ */
34
+ export class VercelAdapter implements DeploymentAdapter {
35
+ readonly name = 'vercel'
36
+
37
+ createContentSource (_config: MkdnSiteConfig): ContentSource {
38
+ // TODO: Implement Vercel Blob-backed or GitHub-backed source
39
+ throw new Error(
40
+ 'VercelAdapter.createContentSource() not yet implemented. ' +
41
+ 'Provide a Blob-backed or GitHub-backed ContentSource.'
42
+ )
43
+ }
44
+
45
+ async createRenderer (_config: MkdnSiteConfig): Promise<MarkdownRenderer> {
46
+ return await createRenderer('portable')
47
+ }
48
+ }