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 +21 -0
- package/README.md +211 -0
- package/package.json +72 -0
- package/src/adapters/cloudflare.ts +85 -0
- package/src/adapters/fly.ts +22 -0
- package/src/adapters/local.ts +153 -0
- package/src/adapters/netlify.ts +17 -0
- package/src/adapters/types.ts +54 -0
- package/src/adapters/vercel.ts +48 -0
- package/src/cli.ts +140 -0
- package/src/client/scripts.ts +106 -0
- package/src/config/defaults.ts +68 -0
- package/src/config/schema.ts +192 -0
- package/src/content/filesystem.ts +210 -0
- package/src/content/frontmatter.ts +66 -0
- package/src/content/github.ts +211 -0
- package/src/content/types.ts +86 -0
- package/src/discovery/llmstxt.ts +70 -0
- package/src/handler.ts +188 -0
- package/src/index.ts +57 -0
- package/src/negotiate/accept.ts +72 -0
- package/src/negotiate/headers.ts +56 -0
- package/src/render/bun-native.ts +54 -0
- package/src/render/components/index.ts +149 -0
- package/src/render/page-shell.ts +121 -0
- package/src/render/portable.ts +222 -0
- package/src/render/types.ts +74 -0
- package/src/theme/prose-css.ts +377 -0
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
|
+
}
|