contentbase 0.1.8 → 0.4.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 (40) hide show
  1. package/CNAME +1 -0
  2. package/LAUNCH-PLAN.md +419 -0
  3. package/README.md +90 -0
  4. package/index.html +1636 -0
  5. package/package.json +5 -4
  6. package/src/adapters/node-fs.ts +80 -0
  7. package/src/cli/commands/api/endpoints/doc.ts +2 -2
  8. package/src/cli/commands/api/endpoints/document.ts +2 -2
  9. package/src/cli/commands/api/endpoints/search-reindex.ts +6 -25
  10. package/src/cli/commands/api/endpoints/search-status.ts +3 -20
  11. package/src/cli/commands/api/endpoints/search.ts +3 -33
  12. package/src/cli/commands/api/endpoints/semantic-search.ts +3 -33
  13. package/src/cli/commands/embed.ts +8 -81
  14. package/src/cli/commands/mcp.ts +5 -1128
  15. package/src/cli/commands/search.ts +25 -90
  16. package/src/cli/commands/serve.ts +7 -32
  17. package/src/cli/index.ts +1 -1
  18. package/src/cli/load-collection.ts +2 -2
  19. package/src/collection.ts +42 -33
  20. package/src/document.ts +18 -0
  21. package/src/extract-sections.ts +3 -0
  22. package/src/index.ts +10 -0
  23. package/src/mcp/helpers.ts +7 -0
  24. package/src/mcp/model-info.ts +102 -0
  25. package/src/mcp/prompts.ts +199 -0
  26. package/src/mcp/readme.ts +153 -0
  27. package/src/mcp/resources.ts +89 -0
  28. package/src/mcp/server.ts +103 -0
  29. package/src/mcp/tools/mutation.ts +176 -0
  30. package/src/mcp/tools/query.ts +141 -0
  31. package/src/mcp/tools/search.ts +180 -0
  32. package/src/parse.ts +5 -0
  33. package/src/query/query-dsl.ts +1 -1
  34. package/src/relationships/has-many.ts +4 -2
  35. package/src/search/document-inputs.ts +85 -0
  36. package/src/search/luca-semantic-search.ts +75 -0
  37. package/src/types.ts +54 -0
  38. package/src/utils/index.ts +2 -0
  39. package/src/utils/match-pattern.ts +2 -2
  40. package/src/utils/strip-markdown.ts +59 -0
package/index.html ADDED
@@ -0,0 +1,1636 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Contentbase — An ORM for Markdown</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
+ <style>
11
+ /* ============ THEME (Dark-first: Neon Dashboard) ============ */
12
+ :root {
13
+ --font-body: 'Space Grotesk', system-ui, sans-serif;
14
+ --font-mono: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
15
+
16
+ --bg: #0c0e14;
17
+ --surface: #13161f;
18
+ --surface2: #1a1e2a;
19
+ --surface-elevated: #1e2233;
20
+ --border: rgba(255, 255, 255, 0.06);
21
+ --border-bright: rgba(255, 255, 255, 0.12);
22
+ --text: #e8ecf4;
23
+ --text-dim: #6b7394;
24
+
25
+ --cyan: #22d3ee;
26
+ --cyan-dim: rgba(34, 211, 238, 0.10);
27
+ --purple: #a78bfa;
28
+ --purple-dim: rgba(167, 139, 250, 0.10);
29
+ --green: #34d399;
30
+ --green-dim: rgba(52, 211, 153, 0.10);
31
+ --amber: #fbbf24;
32
+ --amber-dim: rgba(251, 191, 36, 0.10);
33
+ --rose: #fb7185;
34
+ --rose-dim: rgba(251, 113, 133, 0.10);
35
+ --blue: #60a5fa;
36
+ --blue-dim: rgba(96, 165, 250, 0.10);
37
+
38
+ --accent: var(--cyan);
39
+ --accent-dim: var(--cyan-dim);
40
+ }
41
+
42
+ @media (prefers-color-scheme: light) {
43
+ :root {
44
+ --bg: #f5f7fa;
45
+ --surface: #ffffff;
46
+ --surface2: #eef1f6;
47
+ --surface-elevated: #ffffff;
48
+ --border: rgba(0, 0, 0, 0.07);
49
+ --border-bright: rgba(0, 0, 0, 0.14);
50
+ --text: #1a1e2e;
51
+ --text-dim: #6b7394;
52
+
53
+ --cyan: #0891b2;
54
+ --cyan-dim: rgba(8, 145, 178, 0.08);
55
+ --purple: #7c3aed;
56
+ --purple-dim: rgba(124, 58, 237, 0.08);
57
+ --green: #059669;
58
+ --green-dim: rgba(5, 150, 105, 0.08);
59
+ --amber: #d97706;
60
+ --amber-dim: rgba(217, 119, 6, 0.08);
61
+ --rose: #e11d48;
62
+ --rose-dim: rgba(225, 29, 72, 0.08);
63
+ --blue: #2563eb;
64
+ --blue-dim: rgba(37, 99, 235, 0.08);
65
+ }
66
+ }
67
+
68
+ /* ============ RESET + BASE ============ */
69
+ * { margin: 0; padding: 0; box-sizing: border-box; }
70
+
71
+ body {
72
+ background: var(--bg);
73
+ background-image:
74
+ radial-gradient(ellipse at 25% 0%, var(--purple-dim) 0%, transparent 50%),
75
+ radial-gradient(ellipse at 75% 10%, var(--cyan-dim) 0%, transparent 40%),
76
+ radial-gradient(ellipse at 50% 100%, var(--green-dim) 0%, transparent 50%);
77
+ color: var(--text);
78
+ font-family: var(--font-body);
79
+ padding: 40px;
80
+ min-height: 100vh;
81
+ overflow-wrap: break-word;
82
+ }
83
+
84
+ /* ============ ANIMATIONS ============ */
85
+ @keyframes fadeUp {
86
+ from { opacity: 0; transform: translateY(16px); }
87
+ to { opacity: 1; transform: translateY(0); }
88
+ }
89
+
90
+ @keyframes fadeScale {
91
+ from { opacity: 0; transform: scale(0.92); }
92
+ to { opacity: 1; transform: scale(1); }
93
+ }
94
+
95
+ @keyframes shimmer {
96
+ 0% { background-position: -200% center; }
97
+ 100% { background-position: 200% center; }
98
+ }
99
+
100
+ @keyframes pulse-glow {
101
+ 0%, 100% { opacity: 0.4; }
102
+ 50% { opacity: 1; }
103
+ }
104
+
105
+ .anim-up {
106
+ animation: fadeUp 0.5s ease-out both;
107
+ animation-delay: calc(var(--i, 0) * 0.06s);
108
+ }
109
+
110
+ .anim-scale {
111
+ animation: fadeScale 0.4s ease-out both;
112
+ animation-delay: calc(var(--i, 0) * 0.06s);
113
+ }
114
+
115
+ @media (prefers-reduced-motion: reduce) {
116
+ *, *::before, *::after {
117
+ animation-duration: 0.01ms !important;
118
+ animation-delay: 0ms !important;
119
+ transition-duration: 0.01ms !important;
120
+ }
121
+ }
122
+
123
+ /* ============ LAYOUT ============ */
124
+ .wrap {
125
+ max-width: 1400px;
126
+ margin: 0 auto;
127
+ display: grid;
128
+ grid-template-columns: 170px 1fr;
129
+ gap: 0 40px;
130
+ }
131
+ .main { min-width: 0; }
132
+
133
+ /* ============ TOC NAV ============ */
134
+ .toc {
135
+ position: sticky;
136
+ top: 24px;
137
+ align-self: start;
138
+ padding: 14px 0;
139
+ grid-row: 1 / -1;
140
+ max-height: calc(100dvh - 48px);
141
+ overflow-y: auto;
142
+ }
143
+ .toc::-webkit-scrollbar { width: 3px; }
144
+ .toc::-webkit-scrollbar-thumb { background: var(--surface-elevated); border-radius: 2px; }
145
+
146
+ .toc-title {
147
+ font-family: var(--font-mono);
148
+ font-size: 9px;
149
+ font-weight: 700;
150
+ text-transform: uppercase;
151
+ letter-spacing: 2px;
152
+ color: var(--text-dim);
153
+ padding: 0 0 10px;
154
+ margin-bottom: 8px;
155
+ border-bottom: 1px solid var(--border);
156
+ }
157
+
158
+ .toc a {
159
+ display: block;
160
+ font-size: 11px;
161
+ color: var(--text-dim);
162
+ text-decoration: none;
163
+ padding: 4px 8px;
164
+ border-radius: 5px;
165
+ border-left: 2px solid transparent;
166
+ transition: all 0.15s;
167
+ line-height: 1.4;
168
+ margin-bottom: 1px;
169
+ }
170
+ .toc a:hover { color: var(--text); background: var(--surface2); }
171
+ .toc a.active { color: var(--cyan); border-left-color: var(--cyan); }
172
+
173
+ @media (max-width: 1000px) {
174
+ .wrap { grid-template-columns: 1fr; padding-top: 0; }
175
+ body { padding-top: 0; }
176
+ .toc {
177
+ position: sticky;
178
+ top: 0;
179
+ z-index: 200;
180
+ max-height: none;
181
+ display: flex;
182
+ gap: 4px;
183
+ align-items: center;
184
+ overflow-x: auto;
185
+ -webkit-overflow-scrolling: touch;
186
+ background: var(--bg);
187
+ border-bottom: 1px solid var(--border);
188
+ padding: 10px 0;
189
+ margin: 0 -40px;
190
+ padding-left: 40px;
191
+ padding-right: 40px;
192
+ grid-row: auto;
193
+ }
194
+ .toc::-webkit-scrollbar { display: none; }
195
+ .toc-title { display: none; }
196
+ .toc a {
197
+ white-space: nowrap;
198
+ flex-shrink: 0;
199
+ border-left: none;
200
+ border-bottom: 2px solid transparent;
201
+ border-radius: 4px 4px 0 0;
202
+ padding: 6px 10px;
203
+ font-size: 10px;
204
+ }
205
+ .toc a.active {
206
+ border-left: none;
207
+ border-bottom-color: var(--cyan);
208
+ background: var(--surface);
209
+ }
210
+ .main { padding-top: 20px; }
211
+ .sec-head { scroll-margin-top: 52px; }
212
+ }
213
+
214
+ /* ============ HERO ============ */
215
+ .hero {
216
+ text-align: center;
217
+ padding: 60px 0 40px;
218
+ }
219
+
220
+ .hero-logo {
221
+ font-family: var(--font-mono);
222
+ font-size: 14px;
223
+ font-weight: 600;
224
+ color: var(--cyan);
225
+ letter-spacing: 3px;
226
+ text-transform: uppercase;
227
+ margin-bottom: 16px;
228
+ display: flex;
229
+ align-items: center;
230
+ justify-content: center;
231
+ gap: 10px;
232
+ }
233
+
234
+ .hero-logo .glyph {
235
+ display: inline-flex;
236
+ width: 32px;
237
+ height: 32px;
238
+ border-radius: 8px;
239
+ background: linear-gradient(135deg, var(--cyan), var(--purple));
240
+ align-items: center;
241
+ justify-content: center;
242
+ font-size: 16px;
243
+ color: #fff;
244
+ font-weight: 700;
245
+ letter-spacing: 0;
246
+ }
247
+
248
+ h1 {
249
+ font-size: clamp(36px, 5vw, 56px);
250
+ font-weight: 700;
251
+ letter-spacing: -2px;
252
+ line-height: 1.1;
253
+ margin-bottom: 16px;
254
+ text-wrap: balance;
255
+ }
256
+
257
+ h1 .gradient-text {
258
+ background: linear-gradient(135deg, var(--cyan), var(--purple), var(--green));
259
+ background-size: 200% auto;
260
+ -webkit-background-clip: text;
261
+ background-clip: text;
262
+ -webkit-text-fill-color: transparent;
263
+ animation: shimmer 6s ease-in-out infinite;
264
+ }
265
+
266
+ .tagline {
267
+ font-size: 18px;
268
+ color: var(--text-dim);
269
+ max-width: 640px;
270
+ margin: 0 auto 28px;
271
+ line-height: 1.6;
272
+ }
273
+
274
+ .tagline em {
275
+ font-style: normal;
276
+ color: var(--text);
277
+ font-weight: 500;
278
+ }
279
+
280
+ /* ============ BADGES ============ */
281
+ .badge-row {
282
+ display: flex;
283
+ gap: 8px;
284
+ justify-content: center;
285
+ flex-wrap: wrap;
286
+ }
287
+
288
+ .badge {
289
+ font-family: var(--font-mono);
290
+ font-size: 11px;
291
+ font-weight: 500;
292
+ padding: 4px 12px;
293
+ border-radius: 20px;
294
+ border: 1px solid var(--border-bright);
295
+ background: var(--surface);
296
+ color: var(--text-dim);
297
+ transition: border-color 0.2s, color 0.2s;
298
+ }
299
+
300
+ .badge:hover { border-color: var(--cyan); color: var(--cyan); }
301
+
302
+ .badge--cyan { border-color: var(--cyan-dim); color: var(--cyan); }
303
+ .badge--purple { border-color: var(--purple-dim); color: var(--purple); }
304
+ .badge--green { border-color: var(--green-dim); color: var(--green); }
305
+ .badge--amber { border-color: var(--amber-dim); color: var(--amber); }
306
+
307
+ /* ============ SECTION HEADINGS ============ */
308
+ .sec-head {
309
+ font-family: var(--font-mono);
310
+ font-size: 11px;
311
+ font-weight: 600;
312
+ text-transform: uppercase;
313
+ letter-spacing: 2px;
314
+ color: var(--text-dim);
315
+ padding: 48px 0 24px;
316
+ display: flex;
317
+ align-items: center;
318
+ gap: 12px;
319
+ scroll-margin-top: 24px;
320
+ }
321
+
322
+ .sec-head .num {
323
+ display: inline-flex;
324
+ width: 24px;
325
+ height: 24px;
326
+ border-radius: 6px;
327
+ font-size: 12px;
328
+ font-weight: 700;
329
+ align-items: center;
330
+ justify-content: center;
331
+ }
332
+
333
+ .sec-head--cyan .num { background: var(--cyan-dim); color: var(--cyan); }
334
+ .sec-head--purple .num { background: var(--purple-dim); color: var(--purple); }
335
+ .sec-head--green .num { background: var(--green-dim); color: var(--green); }
336
+ .sec-head--amber .num { background: var(--amber-dim); color: var(--amber); }
337
+ .sec-head--rose .num { background: var(--rose-dim); color: var(--rose); }
338
+ .sec-head--blue .num { background: var(--blue-dim); color: var(--blue); }
339
+
340
+ /* ============ FLOW PIPELINE ============ */
341
+ .pipeline-wrap {
342
+ background: var(--surface);
343
+ border: 1px solid var(--border);
344
+ border-radius: 16px;
345
+ padding: 32px 24px;
346
+ overflow-x: auto;
347
+ }
348
+
349
+ .pipeline {
350
+ display: flex;
351
+ align-items: center;
352
+ gap: 0;
353
+ min-width: 700px;
354
+ justify-content: center;
355
+ }
356
+
357
+ .pipe-node {
358
+ background: var(--surface2);
359
+ border: 1px solid var(--border-bright);
360
+ border-radius: 12px;
361
+ padding: 16px 20px;
362
+ text-align: center;
363
+ min-width: 140px;
364
+ flex-shrink: 0;
365
+ transition: transform 0.2s, box-shadow 0.2s;
366
+ }
367
+
368
+ .pipe-node:hover {
369
+ transform: translateY(-2px);
370
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
371
+ }
372
+
373
+ .pipe-node .icon {
374
+ font-size: 24px;
375
+ margin-bottom: 6px;
376
+ }
377
+
378
+ .pipe-node .label {
379
+ font-weight: 600;
380
+ font-size: 13px;
381
+ margin-bottom: 2px;
382
+ }
383
+
384
+ .pipe-node .sub {
385
+ font-size: 11px;
386
+ color: var(--text-dim);
387
+ font-family: var(--font-mono);
388
+ }
389
+
390
+ .pipe-node--cyan { border-color: color-mix(in srgb, var(--cyan) 30%, transparent); }
391
+ .pipe-node--cyan .label { color: var(--cyan); }
392
+ .pipe-node--purple { border-color: color-mix(in srgb, var(--purple) 30%, transparent); }
393
+ .pipe-node--purple .label { color: var(--purple); }
394
+ .pipe-node--green { border-color: color-mix(in srgb, var(--green) 30%, transparent); }
395
+ .pipe-node--green .label { color: var(--green); }
396
+ .pipe-node--amber { border-color: color-mix(in srgb, var(--amber) 30%, transparent); }
397
+ .pipe-node--amber .label { color: var(--amber); }
398
+
399
+ .pipe-arrow {
400
+ display: flex;
401
+ align-items: center;
402
+ padding: 0 6px;
403
+ flex-shrink: 0;
404
+ }
405
+
406
+ .pipe-arrow svg {
407
+ width: 32px;
408
+ height: 16px;
409
+ fill: none;
410
+ stroke: var(--border-bright);
411
+ stroke-width: 1.5;
412
+ stroke-linecap: round;
413
+ }
414
+
415
+ /* ============ FEATURE GRID ============ */
416
+ .feature-grid {
417
+ display: grid;
418
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
419
+ gap: 16px;
420
+ }
421
+
422
+ .feature-card {
423
+ background: var(--surface);
424
+ border: 1px solid var(--border);
425
+ border-radius: 12px;
426
+ padding: 20px 22px;
427
+ transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
428
+ position: relative;
429
+ overflow: hidden;
430
+ }
431
+
432
+ .feature-card:hover {
433
+ transform: translateY(-2px);
434
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
435
+ border-color: var(--border-bright);
436
+ }
437
+
438
+ .feature-card .fc-icon {
439
+ width: 36px;
440
+ height: 36px;
441
+ border-radius: 10px;
442
+ display: flex;
443
+ align-items: center;
444
+ justify-content: center;
445
+ font-size: 18px;
446
+ margin-bottom: 14px;
447
+ font-weight: 700;
448
+ font-family: var(--font-mono);
449
+ }
450
+
451
+ .feature-card .fc-title {
452
+ font-weight: 600;
453
+ font-size: 15px;
454
+ margin-bottom: 6px;
455
+ letter-spacing: -0.3px;
456
+ }
457
+
458
+ .feature-card .fc-desc {
459
+ font-size: 13px;
460
+ color: var(--text-dim);
461
+ line-height: 1.6;
462
+ }
463
+
464
+ .feature-card code {
465
+ font-family: var(--font-mono);
466
+ font-size: 11px;
467
+ padding: 1px 5px;
468
+ border-radius: 3px;
469
+ }
470
+
471
+ .fc--cyan .fc-icon { background: var(--cyan-dim); color: var(--cyan); }
472
+ .fc--cyan code { background: var(--cyan-dim); color: var(--cyan); }
473
+ .fc--purple .fc-icon { background: var(--purple-dim); color: var(--purple); }
474
+ .fc--purple code { background: var(--purple-dim); color: var(--purple); }
475
+ .fc--green .fc-icon { background: var(--green-dim); color: var(--green); }
476
+ .fc--green code { background: var(--green-dim); color: var(--green); }
477
+ .fc--amber .fc-icon { background: var(--amber-dim); color: var(--amber); }
478
+ .fc--amber code { background: var(--amber-dim); color: var(--amber); }
479
+ .fc--rose .fc-icon { background: var(--rose-dim); color: var(--rose); }
480
+ .fc--rose code { background: var(--rose-dim); color: var(--rose); }
481
+ .fc--blue .fc-icon { background: var(--blue-dim); color: var(--blue); }
482
+ .fc--blue code { background: var(--blue-dim); color: var(--blue); }
483
+
484
+ /* ============ CODE BLOCK ============ */
485
+ .code-section {
486
+ display: grid;
487
+ grid-template-columns: 1fr 1fr;
488
+ gap: 16px;
489
+ }
490
+
491
+ .code-block {
492
+ background: var(--surface);
493
+ border: 1px solid var(--border);
494
+ border-radius: 12px;
495
+ overflow: hidden;
496
+ }
497
+
498
+ .code-block .cb-header {
499
+ font-family: var(--font-mono);
500
+ font-size: 11px;
501
+ font-weight: 600;
502
+ padding: 10px 18px;
503
+ border-bottom: 1px solid var(--border);
504
+ color: var(--text-dim);
505
+ display: flex;
506
+ align-items: center;
507
+ gap: 8px;
508
+ background: var(--surface2);
509
+ }
510
+
511
+ .code-block .cb-header .dot {
512
+ width: 8px;
513
+ height: 8px;
514
+ border-radius: 50%;
515
+ }
516
+
517
+ .code-block pre {
518
+ padding: 18px;
519
+ overflow-x: auto;
520
+ font-family: var(--font-mono);
521
+ font-size: 12px;
522
+ line-height: 1.7;
523
+ -webkit-overflow-scrolling: touch;
524
+ scrollbar-width: thin;
525
+ scrollbar-color: var(--border) transparent;
526
+ }
527
+
528
+ .code-block pre::-webkit-scrollbar { height: 4px; }
529
+ .code-block pre::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
530
+
531
+ /* Syntax colors */
532
+ .kw { color: var(--purple); } /* keyword */
533
+ .fn { color: var(--cyan); } /* function / method */
534
+ .str { color: var(--green); } /* string */
535
+ .num { color: var(--amber); } /* number */
536
+ .cm { color: var(--text-dim); } /* comment */
537
+ .tp { color: var(--rose); } /* type */
538
+ .pr { color: var(--text); } /* property */
539
+ .op { color: var(--text-dim); } /* operator */
540
+ .p { color: var(--text-dim); } /* punctuation */
541
+
542
+ @media (max-width: 900px) {
543
+ .code-section { grid-template-columns: 1fr; }
544
+ }
545
+
546
+ /* ============ ARCHITECTURE LAYERS ============ */
547
+ .arch-layers {
548
+ display: grid;
549
+ gap: 0;
550
+ }
551
+
552
+ .arch-layer {
553
+ background: var(--surface);
554
+ border: 1px solid var(--border);
555
+ padding: 20px 24px;
556
+ position: relative;
557
+ }
558
+
559
+ .arch-layer:first-child { border-radius: 12px 12px 0 0; }
560
+ .arch-layer:last-child { border-radius: 0 0 12px 12px; }
561
+ .arch-layer + .arch-layer { border-top: none; }
562
+
563
+ .arch-layer .al-label {
564
+ font-family: var(--font-mono);
565
+ font-size: 10px;
566
+ font-weight: 600;
567
+ text-transform: uppercase;
568
+ letter-spacing: 1.5px;
569
+ margin-bottom: 10px;
570
+ display: flex;
571
+ align-items: center;
572
+ gap: 8px;
573
+ }
574
+
575
+ .arch-layer .al-label .dot {
576
+ width: 8px;
577
+ height: 8px;
578
+ border-radius: 50%;
579
+ }
580
+
581
+ .al--cyan .al-label { color: var(--cyan); }
582
+ .al--cyan .al-label .dot { background: var(--cyan); }
583
+ .al--purple .al-label { color: var(--purple); }
584
+ .al--purple .al-label .dot { background: var(--purple); }
585
+ .al--green .al-label { color: var(--green); }
586
+ .al--green .al-label .dot { background: var(--green); }
587
+ .al--amber .al-label { color: var(--amber); }
588
+ .al--amber .al-label .dot { background: var(--amber); }
589
+
590
+ .al-chips {
591
+ display: flex;
592
+ gap: 8px;
593
+ flex-wrap: wrap;
594
+ }
595
+
596
+ .al-chip {
597
+ font-family: var(--font-mono);
598
+ font-size: 11px;
599
+ font-weight: 500;
600
+ padding: 4px 12px;
601
+ border-radius: 6px;
602
+ border: 1px solid var(--border);
603
+ background: var(--surface2);
604
+ color: var(--text);
605
+ transition: border-color 0.2s;
606
+ }
607
+
608
+ .al-chip:hover { border-color: var(--border-bright); }
609
+
610
+ .arch-arrow {
611
+ display: flex;
612
+ justify-content: center;
613
+ padding: 2px 0;
614
+ position: relative;
615
+ z-index: 1;
616
+ }
617
+
618
+ .arch-arrow svg {
619
+ width: 16px;
620
+ height: 12px;
621
+ fill: none;
622
+ stroke: var(--border-bright);
623
+ stroke-width: 1.5;
624
+ stroke-linecap: round;
625
+ }
626
+
627
+ .arch-side {
628
+ background: var(--surface);
629
+ border: 1px solid var(--border);
630
+ border-radius: 12px;
631
+ padding: 20px 24px;
632
+ }
633
+
634
+ .arch-grid {
635
+ display: grid;
636
+ grid-template-columns: 1fr 220px;
637
+ gap: 16px;
638
+ align-items: start;
639
+ }
640
+
641
+ @media (max-width: 900px) {
642
+ .arch-grid { grid-template-columns: 1fr; }
643
+ }
644
+
645
+ /* ============ USE CASES ============ */
646
+ .use-case-grid {
647
+ display: grid;
648
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
649
+ gap: 16px;
650
+ }
651
+
652
+ .use-case {
653
+ background: var(--surface);
654
+ border: 1px solid var(--border);
655
+ border-radius: 12px;
656
+ padding: 24px;
657
+ transition: transform 0.2s, box-shadow 0.2s;
658
+ }
659
+
660
+ .use-case:hover {
661
+ transform: translateY(-2px);
662
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
663
+ }
664
+
665
+ .use-case .uc-icon {
666
+ font-size: 28px;
667
+ margin-bottom: 12px;
668
+ }
669
+
670
+ .use-case .uc-title {
671
+ font-weight: 600;
672
+ font-size: 14px;
673
+ margin-bottom: 6px;
674
+ }
675
+
676
+ .use-case .uc-desc {
677
+ font-size: 12px;
678
+ color: var(--text-dim);
679
+ line-height: 1.6;
680
+ }
681
+
682
+ /* ============ CONCEPT CARDS (how it works) ============ */
683
+ .concept-grid {
684
+ display: grid;
685
+ grid-template-columns: repeat(3, 1fr);
686
+ gap: 16px;
687
+ }
688
+
689
+ .concept-card {
690
+ background: var(--surface);
691
+ border: 1px solid var(--border);
692
+ border-radius: 12px;
693
+ padding: 20px;
694
+ text-align: center;
695
+ }
696
+
697
+ .concept-card .cc-num {
698
+ font-family: var(--font-mono);
699
+ font-size: 32px;
700
+ font-weight: 700;
701
+ line-height: 1;
702
+ margin-bottom: 8px;
703
+ }
704
+
705
+ .concept-card .cc-title {
706
+ font-weight: 600;
707
+ font-size: 14px;
708
+ margin-bottom: 6px;
709
+ }
710
+
711
+ .concept-card .cc-desc {
712
+ font-size: 12px;
713
+ color: var(--text-dim);
714
+ line-height: 1.5;
715
+ }
716
+
717
+ @media (max-width: 768px) {
718
+ body { padding: 20px; }
719
+ .concept-grid { grid-template-columns: 1fr; }
720
+ .feature-grid { grid-template-columns: 1fr; }
721
+ .pipeline { min-width: auto; flex-wrap: wrap; gap: 8px; justify-content: center; }
722
+ .pipe-arrow { display: none; }
723
+ .use-case-grid { grid-template-columns: 1fr; }
724
+ }
725
+
726
+ /* ============ FOOTER ============ */
727
+ .footer {
728
+ text-align: center;
729
+ padding: 48px 0 24px;
730
+ color: var(--text-dim);
731
+ font-size: 12px;
732
+ font-family: var(--font-mono);
733
+ }
734
+
735
+ .footer a {
736
+ color: var(--cyan);
737
+ text-decoration: none;
738
+ }
739
+
740
+ .footer a:hover { text-decoration: underline; }
741
+
742
+ /* ============ MARKDOWN EXAMPLE ============ */
743
+ .md-example {
744
+ background: var(--surface);
745
+ border: 1px solid var(--border);
746
+ border-radius: 12px;
747
+ padding: 24px;
748
+ display: grid;
749
+ grid-template-columns: 1fr 1fr;
750
+ gap: 24px;
751
+ align-items: start;
752
+ }
753
+
754
+ .md-file {
755
+ background: var(--surface2);
756
+ border: 1px solid var(--border);
757
+ border-radius: 10px;
758
+ overflow: hidden;
759
+ }
760
+
761
+ .md-file .mf-header {
762
+ font-family: var(--font-mono);
763
+ font-size: 11px;
764
+ font-weight: 600;
765
+ padding: 8px 14px;
766
+ border-bottom: 1px solid var(--border);
767
+ color: var(--text-dim);
768
+ background: color-mix(in srgb, var(--surface2) 80%, var(--surface) 20%);
769
+ }
770
+
771
+ .md-file pre {
772
+ padding: 14px;
773
+ font-family: var(--font-mono);
774
+ font-size: 11px;
775
+ line-height: 1.65;
776
+ overflow-x: auto;
777
+ color: var(--text);
778
+ }
779
+
780
+ .md-result {
781
+ padding-top: 4px;
782
+ }
783
+
784
+ .md-result .mr-title {
785
+ font-family: var(--font-mono);
786
+ font-size: 11px;
787
+ font-weight: 600;
788
+ color: var(--cyan);
789
+ text-transform: uppercase;
790
+ letter-spacing: 1.5px;
791
+ margin-bottom: 14px;
792
+ }
793
+
794
+ .mr-item {
795
+ margin-bottom: 12px;
796
+ }
797
+
798
+ .mr-item .mr-label {
799
+ font-family: var(--font-mono);
800
+ font-size: 10px;
801
+ color: var(--text-dim);
802
+ text-transform: uppercase;
803
+ letter-spacing: 1px;
804
+ margin-bottom: 2px;
805
+ }
806
+
807
+ .mr-item .mr-value {
808
+ font-size: 13px;
809
+ font-weight: 500;
810
+ }
811
+
812
+ .mr-item .mr-value code {
813
+ font-family: var(--font-mono);
814
+ font-size: 11px;
815
+ background: var(--cyan-dim);
816
+ color: var(--cyan);
817
+ padding: 1px 5px;
818
+ border-radius: 3px;
819
+ }
820
+
821
+ .mr-pills {
822
+ display: flex;
823
+ gap: 6px;
824
+ flex-wrap: wrap;
825
+ }
826
+
827
+ .mr-pill {
828
+ font-family: var(--font-mono);
829
+ font-size: 10px;
830
+ padding: 2px 8px;
831
+ border-radius: 4px;
832
+ background: var(--green-dim);
833
+ color: var(--green);
834
+ }
835
+
836
+ @media (max-width: 768px) {
837
+ .md-example { grid-template-columns: 1fr; }
838
+ }
839
+
840
+ /* ============ CALLOUT ============ */
841
+ .callout {
842
+ background: var(--surface);
843
+ border: 1px solid var(--border);
844
+ border-left: 3px solid var(--cyan);
845
+ border-radius: 0 12px 12px 0;
846
+ padding: 18px 22px;
847
+ font-size: 14px;
848
+ line-height: 1.6;
849
+ color: var(--text-dim);
850
+ margin-top: 16px;
851
+ }
852
+
853
+ .callout strong {
854
+ color: var(--text);
855
+ font-weight: 600;
856
+ }
857
+
858
+ /* ============ SERVER APIS (Coming Soon) ============ */
859
+ .server-hero {
860
+ background: var(--surface);
861
+ border: 1px solid var(--border);
862
+ border-radius: 16px;
863
+ padding: 36px 32px;
864
+ position: relative;
865
+ overflow: hidden;
866
+ }
867
+
868
+ .server-hero::before {
869
+ content: '';
870
+ position: absolute;
871
+ inset: 0;
872
+ background:
873
+ radial-gradient(ellipse at 20% 50%, var(--cyan-dim) 0%, transparent 50%),
874
+ radial-gradient(ellipse at 50% 50%, var(--purple-dim) 0%, transparent 50%),
875
+ radial-gradient(ellipse at 80% 50%, var(--green-dim) 0%, transparent 50%);
876
+ pointer-events: none;
877
+ }
878
+
879
+ .server-hero-inner {
880
+ position: relative;
881
+ z-index: 1;
882
+ }
883
+
884
+ .server-hero .sh-badge {
885
+ display: inline-block;
886
+ font-family: var(--font-mono);
887
+ font-size: 10px;
888
+ font-weight: 600;
889
+ text-transform: uppercase;
890
+ letter-spacing: 2px;
891
+ padding: 4px 12px;
892
+ border-radius: 20px;
893
+ background: linear-gradient(135deg, var(--cyan-dim), var(--purple-dim));
894
+ border: 1px solid var(--border-bright);
895
+ color: var(--cyan);
896
+ margin-bottom: 16px;
897
+ }
898
+
899
+ .server-hero .sh-title {
900
+ font-size: clamp(22px, 3vw, 32px);
901
+ font-weight: 700;
902
+ letter-spacing: -1px;
903
+ margin-bottom: 10px;
904
+ line-height: 1.2;
905
+ }
906
+
907
+ .server-hero .sh-subtitle {
908
+ font-size: 15px;
909
+ color: var(--text-dim);
910
+ line-height: 1.6;
911
+ max-width: 600px;
912
+ margin-bottom: 28px;
913
+ }
914
+
915
+ .api-cards {
916
+ display: grid;
917
+ grid-template-columns: repeat(3, 1fr);
918
+ gap: 16px;
919
+ }
920
+
921
+ @media (max-width: 768px) {
922
+ .api-cards { grid-template-columns: 1fr; }
923
+ }
924
+
925
+ .api-card {
926
+ background: color-mix(in srgb, var(--surface) 80%, transparent);
927
+ border: 1px solid var(--border-bright);
928
+ border-radius: 12px;
929
+ padding: 24px 20px;
930
+ text-align: center;
931
+ transition: transform 0.25s, box-shadow 0.25s, border-color 0.25s;
932
+ position: relative;
933
+ }
934
+
935
+ .api-card:hover {
936
+ transform: translateY(-3px);
937
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
938
+ }
939
+
940
+ .api-card--cyan:hover { border-color: var(--cyan); }
941
+ .api-card--purple:hover { border-color: var(--purple); }
942
+ .api-card--green:hover { border-color: var(--green); }
943
+
944
+ .api-card .ac-icon {
945
+ width: 56px;
946
+ height: 56px;
947
+ border-radius: 16px;
948
+ display: flex;
949
+ align-items: center;
950
+ justify-content: center;
951
+ margin: 0 auto 16px;
952
+ font-size: 24px;
953
+ font-weight: 700;
954
+ font-family: var(--font-mono);
955
+ }
956
+
957
+ .api-card--cyan .ac-icon {
958
+ background: linear-gradient(135deg, var(--cyan-dim), color-mix(in srgb, var(--cyan-dim) 50%, transparent));
959
+ color: var(--cyan);
960
+ border: 1px solid color-mix(in srgb, var(--cyan) 20%, transparent);
961
+ }
962
+
963
+ .api-card--purple .ac-icon {
964
+ background: linear-gradient(135deg, var(--purple-dim), color-mix(in srgb, var(--purple-dim) 50%, transparent));
965
+ color: var(--purple);
966
+ border: 1px solid color-mix(in srgb, var(--purple) 20%, transparent);
967
+ }
968
+
969
+ .api-card--green .ac-icon {
970
+ background: linear-gradient(135deg, var(--green-dim), color-mix(in srgb, var(--green-dim) 50%, transparent));
971
+ color: var(--green);
972
+ border: 1px solid color-mix(in srgb, var(--green) 20%, transparent);
973
+ }
974
+
975
+ .api-card .ac-title {
976
+ font-weight: 700;
977
+ font-size: 16px;
978
+ margin-bottom: 4px;
979
+ letter-spacing: -0.3px;
980
+ }
981
+
982
+ .api-card--cyan .ac-title { color: var(--cyan); }
983
+ .api-card--purple .ac-title { color: var(--purple); }
984
+ .api-card--green .ac-title { color: var(--green); }
985
+
986
+ .api-card .ac-desc {
987
+ font-size: 12px;
988
+ color: var(--text-dim);
989
+ line-height: 1.6;
990
+ margin-bottom: 14px;
991
+ }
992
+
993
+ .api-card .ac-endpoints {
994
+ list-style: none;
995
+ text-align: left;
996
+ font-size: 11px;
997
+ line-height: 1.8;
998
+ }
999
+
1000
+ .api-card .ac-endpoints li {
1001
+ padding-left: 14px;
1002
+ position: relative;
1003
+ color: var(--text-dim);
1004
+ }
1005
+
1006
+ .api-card .ac-endpoints li::before {
1007
+ content: '>';
1008
+ position: absolute;
1009
+ left: 0;
1010
+ font-weight: 600;
1011
+ font-family: var(--font-mono);
1012
+ }
1013
+
1014
+ .api-card--cyan .ac-endpoints li::before { color: var(--cyan); }
1015
+ .api-card--purple .ac-endpoints li::before { color: var(--purple); }
1016
+ .api-card--green .ac-endpoints li::before { color: var(--green); }
1017
+
1018
+ .api-card .ac-endpoints code {
1019
+ font-family: var(--font-mono);
1020
+ font-size: 10px;
1021
+ padding: 1px 5px;
1022
+ border-radius: 3px;
1023
+ }
1024
+
1025
+ .api-card--cyan .ac-endpoints code { background: var(--cyan-dim); color: var(--cyan); }
1026
+ .api-card--purple .ac-endpoints code { background: var(--purple-dim); color: var(--purple); }
1027
+ .api-card--green .ac-endpoints code { background: var(--green-dim); color: var(--green); }
1028
+
1029
+ .server-tagline {
1030
+ text-align: center;
1031
+ margin-top: 20px;
1032
+ font-size: 13px;
1033
+ color: var(--text-dim);
1034
+ font-family: var(--font-mono);
1035
+ letter-spacing: 0.3px;
1036
+ }
1037
+
1038
+ .server-tagline strong {
1039
+ color: var(--text);
1040
+ }
1041
+
1042
+ @keyframes glow-pulse {
1043
+ 0%, 100% { box-shadow: 0 0 8px var(--cyan-dim); }
1044
+ 50% { box-shadow: 0 0 20px var(--cyan-dim), 0 0 40px var(--purple-dim); }
1045
+ }
1046
+
1047
+ .server-hero { animation: glow-pulse 4s ease-in-out infinite; }
1048
+
1049
+ @media (prefers-reduced-motion: reduce) {
1050
+ .server-hero { animation: none; }
1051
+ }
1052
+
1053
+ /* ============ CLI GRID ============ */
1054
+ .cli-grid {
1055
+ display: grid;
1056
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1057
+ gap: 8px;
1058
+ }
1059
+
1060
+ .cli-cmd {
1061
+ background: var(--surface);
1062
+ border: 1px solid var(--border);
1063
+ border-radius: 8px;
1064
+ padding: 10px 14px;
1065
+ font-family: var(--font-mono);
1066
+ font-size: 12px;
1067
+ display: flex;
1068
+ align-items: center;
1069
+ gap: 8px;
1070
+ transition: border-color 0.2s;
1071
+ }
1072
+
1073
+ .cli-cmd:hover { border-color: var(--border-bright); }
1074
+
1075
+ .cli-cmd .cmd-name {
1076
+ color: var(--cyan);
1077
+ font-weight: 600;
1078
+ }
1079
+
1080
+ .cli-cmd .cmd-desc {
1081
+ color: var(--text-dim);
1082
+ font-size: 10px;
1083
+ font-family: var(--font-body);
1084
+ }
1085
+ </style>
1086
+ </head>
1087
+ <body>
1088
+
1089
+ <div class="wrap">
1090
+
1091
+ <!-- ===== TOC ===== -->
1092
+ <nav class="toc" id="toc">
1093
+ <div class="toc-title">Contents</div>
1094
+ <a href="#s1">How It Works</a>
1095
+ <a href="#s2">Core Flow</a>
1096
+ <a href="#s3">Features</a>
1097
+ <a href="#s4">Code Examples</a>
1098
+ <a href="#s5">Architecture</a>
1099
+ <a href="#s6">CLI Toolkit</a>
1100
+ <a href="#s7">Server APIs</a>
1101
+ <a href="#s8">Use Cases</a>
1102
+ </nav>
1103
+
1104
+ <div class="main">
1105
+
1106
+ <!-- ===== HERO ===== -->
1107
+ <div class="hero anim-up" style="--i:0">
1108
+ <div class="hero-logo">
1109
+ <span class="glyph">CB</span>
1110
+ CONTENTBASE
1111
+ </div>
1112
+ <h1>An ORM for <span class="gradient-text">Markdown</span></h1>
1113
+ <p class="tagline">
1114
+ Treat your folder of markdown files as a <em>typed, queryable database</em>.
1115
+ No database. No build step. Your content is the source of truth.
1116
+ Get a <em>REST API, GraphQL API, and MCP server</em> for free.
1117
+ </p>
1118
+ <div class="badge-row">
1119
+ <span class="badge badge--cyan">Bun</span>
1120
+ <span class="badge badge--purple">TypeScript</span>
1121
+ <span class="badge badge--green">Zod</span>
1122
+ <span class="badge badge--amber">unified / remark</span>
1123
+ <span class="badge">ESM</span>
1124
+ </div>
1125
+ </div>
1126
+
1127
+ <!-- ===== S1: HOW IT WORKS ===== -->
1128
+ <div id="s1" class="sec-head sec-head--cyan anim-up" style="--i:2"><span class="num">1</span> How It Works</div>
1129
+
1130
+ <div class="md-example anim-up" style="--i:3">
1131
+ <div>
1132
+ <div class="md-file">
1133
+ <div class="mf-header">stories/authentication/user-can-register.mdx</div>
1134
+ <pre><span class="cm">---</span>
1135
+ <span class="pr">status</span>: <span class="str">draft</span>
1136
+ <span class="pr">points</span>: <span class="num">5</span>
1137
+ <span class="pr">epic</span>: <span class="str">authentication</span>
1138
+ <span class="cm">---</span>
1139
+
1140
+ <span class="kw"># User Can Register</span>
1141
+
1142
+ New users can create an account using
1143
+ their email and password.
1144
+
1145
+ <span class="kw">## Acceptance Criteria</span>
1146
+ - Users can sign up with email
1147
+ - Password must be 8+ characters
1148
+ - Validation errors shown inline
1149
+
1150
+ <span class="kw">## Mockups</span>
1151
+ - [Sign Up Form](./mockups/signup.png)
1152
+ - [Error States](./mockups/errors.png)</pre>
1153
+ </div>
1154
+ </div>
1155
+ <div class="md-result">
1156
+ <div class="mr-title">What Contentbase Sees</div>
1157
+ <div class="mr-item">
1158
+ <div class="mr-label">ID</div>
1159
+ <div class="mr-value"><code>stories/authentication/user-can-register</code></div>
1160
+ </div>
1161
+ <div class="mr-item">
1162
+ <div class="mr-label">Title</div>
1163
+ <div class="mr-value">User Can Register</div>
1164
+ </div>
1165
+ <div class="mr-item">
1166
+ <div class="mr-label">Meta (validated)</div>
1167
+ <div class="mr-pills">
1168
+ <span class="mr-pill">status: "draft"</span>
1169
+ <span class="mr-pill">points: 5</span>
1170
+ <span class="mr-pill">epic: "authentication"</span>
1171
+ </div>
1172
+ </div>
1173
+ <div class="mr-item">
1174
+ <div class="mr-label">Sections (typed)</div>
1175
+ <div class="mr-value" style="font-size:12px; line-height:1.7">
1176
+ <code>acceptanceCriteria</code> &rarr; <span style="color:var(--text-dim)">string[]</span><br>
1177
+ <code>mockups</code> &rarr; <span style="color:var(--text-dim)">Record&lt;string, string&gt;</span>
1178
+ </div>
1179
+ </div>
1180
+ <div class="mr-item">
1181
+ <div class="mr-label">Relationships</div>
1182
+ <div class="mr-value" style="font-size:12px">
1183
+ <code>epic</code> &rarr; <span style="color:var(--text-dim)">belongsTo(Epic)</span>
1184
+ </div>
1185
+ </div>
1186
+ <div class="mr-item">
1187
+ <div class="mr-label">Path Pattern</div>
1188
+ <div class="mr-value" style="font-size:12px">
1189
+ <code>stories/:epic/:slug</code> <span style="color:var(--text-dim)">&rarr; auto-infers meta</span>
1190
+ </div>
1191
+ </div>
1192
+ </div>
1193
+ </div>
1194
+
1195
+ <!-- ===== S2: CORE FLOW ===== -->
1196
+ <div id="s2" class="sec-head sec-head--purple anim-up" style="--i:4"><span class="num">2</span> Core Flow</div>
1197
+
1198
+ <div class="pipeline-wrap anim-up" style="--i:5">
1199
+ <div class="pipeline">
1200
+ <div class="pipe-node pipe-node--cyan">
1201
+ <div class="icon">
1202
+ <svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
1203
+ </div>
1204
+ <div class="label">Files on Disk</div>
1205
+ <div class="sub">.md / .mdx</div>
1206
+ </div>
1207
+ <div class="pipe-arrow"><svg viewBox="0 0 32 16"><path d="M4 8 L24 8 M20 4 L24 8 L20 12"/></svg></div>
1208
+ <div class="pipe-node pipe-node--purple">
1209
+ <div class="icon">
1210
+ <svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M4 7V4a2 2 0 012-2h8.5L20 7.5V20a2 2 0 01-2 2H6a2 2 0 01-2-2v-3M9 12h6M9 16h4"/><path d="M14 2v4a2 2 0 002 2h4"/></svg>
1211
+ </div>
1212
+ <div class="label">Collection</div>
1213
+ <div class="sub">load + parse</div>
1214
+ </div>
1215
+ <div class="pipe-arrow"><svg viewBox="0 0 32 16"><path d="M4 8 L24 8 M20 4 L24 8 L20 12"/></svg></div>
1216
+ <div class="pipe-node pipe-node--green">
1217
+ <div class="icon">
1218
+ <svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
1219
+ </div>
1220
+ <div class="label">Model Instances</div>
1221
+ <div class="sub">typed + validated</div>
1222
+ </div>
1223
+ <div class="pipe-arrow"><svg viewBox="0 0 32 16"><path d="M4 8 L24 8 M20 4 L24 8 L20 12"/></svg></div>
1224
+ <div class="pipe-node pipe-node--amber">
1225
+ <div class="icon">
1226
+ <svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M21 21l-4.35-4.35M11 19a8 8 0 100-16 8 8 0 000 16z"/></svg>
1227
+ </div>
1228
+ <div class="label">Query + Extract</div>
1229
+ <div class="sub">.where().fetchAll()</div>
1230
+ </div>
1231
+ </div>
1232
+ </div>
1233
+
1234
+ <div class="concept-grid anim-up" style="--i:6; margin-top: 16px">
1235
+ <div class="concept-card">
1236
+ <div class="cc-num" style="color:var(--cyan)">01</div>
1237
+ <div class="cc-title">Define Models</div>
1238
+ <div class="cc-desc">Describe your content types with Zod schemas, sections, and relationships</div>
1239
+ </div>
1240
+ <div class="concept-card">
1241
+ <div class="cc-num" style="color:var(--purple)">02</div>
1242
+ <div class="cc-title">Load Content</div>
1243
+ <div class="cc-desc">Point a Collection at a folder. Every markdown file becomes a typed Document</div>
1244
+ </div>
1245
+ <div class="concept-card">
1246
+ <div class="cc-num" style="color:var(--green)">03</div>
1247
+ <div class="cc-title">Query Anything</div>
1248
+ <div class="cc-desc">Filter by meta, traverse relationships, extract sections, validate schemas</div>
1249
+ </div>
1250
+ </div>
1251
+
1252
+ <!-- ===== S3: FEATURES ===== -->
1253
+ <div id="s3" class="sec-head sec-head--green anim-up" style="--i:7"><span class="num">3</span> Features</div>
1254
+
1255
+ <div class="feature-grid">
1256
+ <div class="feature-card fc--cyan anim-scale" style="--i:8">
1257
+ <div class="fc-icon">Z</div>
1258
+ <div class="fc-title">Schema Validation</div>
1259
+ <div class="fc-desc">Zod schemas validate frontmatter. Catch typos, enforce types, get defaults for free. Full error reporting with field-level detail.</div>
1260
+ </div>
1261
+ <div class="feature-card fc--purple anim-scale" style="--i:9">
1262
+ <div class="fc-icon">{}</div>
1263
+ <div class="fc-title">Typed Sections</div>
1264
+ <div class="fc-desc">Extract structured data from markdown headings. Lists become <code>string[]</code>, links become <code>Record</code>, tables become objects.</div>
1265
+ </div>
1266
+ <div class="feature-card fc--green anim-scale" style="--i:10">
1267
+ <div class="fc-icon">&harr;</div>
1268
+ <div class="fc-title">Relationships</div>
1269
+ <div class="fc-desc"><code>hasMany</code> / <code>belongsTo</code> links between documents. An Epic has many Stories, a Story belongs to an Epic.</div>
1270
+ </div>
1271
+ <div class="feature-card fc--amber anim-scale" style="--i:11">
1272
+ <div class="fc-icon">?</div>
1273
+ <div class="fc-title">Fluent Queries</div>
1274
+ <div class="fc-desc">SQL-like <code>.where().fetchAll()</code> with 12+ operators: eq, gt, contains, regex, in, exists, startsWith, and more.</div>
1275
+ </div>
1276
+ <div class="feature-card fc--rose anim-scale" style="--i:12">
1277
+ <div class="fc-icon">/:</div>
1278
+ <div class="fc-title">Path Patterns</div>
1279
+ <div class="fc-desc">Express-style <code>:param</code> patterns auto-infer meta from file paths. <code>stories/:epic/:slug</code> extracts both values.</div>
1280
+ </div>
1281
+ <div class="feature-card fc--blue anim-scale" style="--i:13">
1282
+ <div class="fc-icon">
1283
+ <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
1284
+ </div>
1285
+ <div class="fc-title">Immutable AST Ops</div>
1286
+ <div class="fc-desc">Insert, remove, replace sections. Returns new Document instances by default. Opt into mutation when you need it.</div>
1287
+ </div>
1288
+ <div class="feature-card fc--cyan anim-scale" style="--i:14">
1289
+ <div class="fc-icon">fn</div>
1290
+ <div class="fc-title">Computed Properties</div>
1291
+ <div class="fc-desc">Lazy-evaluated derived values from document data. Defined once, cached automatically, fully typed.</div>
1292
+ </div>
1293
+ <div class="feature-card fc--purple anim-scale" style="--i:15">
1294
+ <div class="fc-icon">&gt;_</div>
1295
+ <div class="fc-title">CLI Toolkit</div>
1296
+ <div class="fc-desc">9 built-in commands for scaffolding, validation, inspection, extraction, and documentation generation.</div>
1297
+ </div>
1298
+ <div class="feature-card fc--green anim-scale" style="--i:16">
1299
+ <div class="fc-icon">()</div>
1300
+ <div class="fc-title">Standalone Parsing</div>
1301
+ <div class="fc-desc"><code>parse()</code> any markdown string or file without a Collection. Full AST query, sections, and node shortcuts.</div>
1302
+ </div>
1303
+ <div class="feature-card fc--amber anim-scale" style="--i:17">
1304
+ <div class="fc-icon">TS</div>
1305
+ <div class="fc-title">Full TypeScript</div>
1306
+ <div class="fc-desc">Zero manual type annotations needed. Full inference flows from model definitions through queries to results.</div>
1307
+ </div>
1308
+ </div>
1309
+
1310
+ <!-- ===== S4: CODE EXAMPLES ===== -->
1311
+ <div id="s4" class="sec-head sec-head--amber anim-up" style="--i:18"><span class="num">4</span> Code Examples</div>
1312
+
1313
+ <div class="code-section anim-up" style="--i:19">
1314
+ <div class="code-block">
1315
+ <div class="cb-header">
1316
+ <span class="dot" style="background:var(--purple)"></span>
1317
+ Define a Model
1318
+ </div>
1319
+ <pre><span class="kw">const</span> Story = <span class="fn">defineModel</span>(<span class="str">"Story"</span>, {
1320
+ <span class="pr">prefix</span>: <span class="str">"stories"</span>,
1321
+ <span class="pr">pattern</span>: <span class="str">"stories/:epic/:slug"</span>,
1322
+ <span class="pr">meta</span>: z.<span class="fn">object</span>({
1323
+ <span class="pr">status</span>: z.<span class="fn">enum</span>([<span class="str">"draft"</span>, <span class="str">"ready"</span>, <span class="str">"shipped"</span>])
1324
+ .<span class="fn">default</span>(<span class="str">"draft"</span>),
1325
+ <span class="pr">points</span>: z.<span class="fn">number</span>().<span class="fn">optional</span>(),
1326
+ }),
1327
+ <span class="pr">sections</span>: {
1328
+ <span class="pr">acceptanceCriteria</span>: <span class="fn">section</span>(
1329
+ <span class="str">"Acceptance Criteria"</span>, {
1330
+ <span class="pr">extract</span>: (<span class="pr">q</span>) <span class="op">=></span>
1331
+ q.<span class="fn">selectAll</span>(<span class="str">"listItem"</span>)
1332
+ .<span class="fn">map</span>(toString),
1333
+ <span class="pr">schema</span>: z.<span class="fn">array</span>(z.<span class="fn">string</span>()),
1334
+ }),
1335
+ },
1336
+ <span class="pr">relationships</span>: {
1337
+ <span class="pr">epic</span>: <span class="fn">belongsTo</span>(() <span class="op">=></span> Epic, {
1338
+ <span class="pr">foreignKey</span>: (<span class="pr">doc</span>) <span class="op">=></span> doc.meta.epic,
1339
+ }),
1340
+ },
1341
+ });</pre>
1342
+ </div>
1343
+ <div class="code-block">
1344
+ <div class="cb-header">
1345
+ <span class="dot" style="background:var(--cyan)"></span>
1346
+ Load + Query
1347
+ </div>
1348
+ <pre><span class="cm">// Set up collection</span>
1349
+ <span class="kw">const</span> collection = <span class="kw">new</span> <span class="tp">Collection</span>({
1350
+ <span class="pr">rootPath</span>: <span class="str">"./content"</span>
1351
+ });
1352
+ collection.<span class="fn">register</span>(Epic);
1353
+ collection.<span class="fn">register</span>(Story);
1354
+ <span class="kw">await</span> collection.<span class="fn">load</span>();
1355
+
1356
+ <span class="cm">// Query like a database</span>
1357
+ <span class="kw">const</span> drafts = <span class="kw">await</span> collection
1358
+ .<span class="fn">query</span>(Story)
1359
+ .<span class="fn">where</span>(<span class="str">"meta.status"</span>, <span class="str">"draft"</span>)
1360
+ .<span class="fn">whereGt</span>(<span class="str">"meta.points"</span>, <span class="num">3</span>)
1361
+ .<span class="fn">fetchAll</span>();
1362
+
1363
+ <span class="cm">// Traverse relationships</span>
1364
+ <span class="kw">const</span> epic = drafts[<span class="num">0</span>]
1365
+ .relationships.epic.<span class="fn">fetch</span>();
1366
+
1367
+ <span class="cm">// Extract typed sections</span>
1368
+ <span class="kw">const</span> criteria = drafts[<span class="num">0</span>]
1369
+ .sections.acceptanceCriteria;
1370
+ <span class="cm">// => ["Users can sign up...", ...]</span>
1371
+
1372
+ <span class="cm">// Validate against schema</span>
1373
+ <span class="kw">const</span> result = <span class="kw">await</span> drafts[<span class="num">0</span>]
1374
+ .<span class="fn">validate</span>();
1375
+ <span class="cm">// => { valid: true, errors: [] }</span></pre>
1376
+ </div>
1377
+ </div>
1378
+
1379
+ <!-- ===== S5: ARCHITECTURE ===== -->
1380
+ <div id="s5" class="sec-head sec-head--rose anim-up" style="--i:20"><span class="num">5</span> Architecture</div>
1381
+
1382
+ <div class="arch-grid anim-up" style="--i:21">
1383
+ <div class="arch-layers">
1384
+ <div class="arch-layer al--amber">
1385
+ <div class="al-label"><span class="dot"></span> Your Content API</div>
1386
+ <div class="al-chips">
1387
+ <span class="al-chip">Collection</span>
1388
+ <span class="al-chip">CollectionQuery</span>
1389
+ <span class="al-chip">defineModel()</span>
1390
+ <span class="al-chip">section()</span>
1391
+ <span class="al-chip">hasMany()</span>
1392
+ <span class="al-chip">belongsTo()</span>
1393
+ <span class="al-chip">parse()</span>
1394
+ </div>
1395
+ </div>
1396
+ <div class="arch-layer al--green">
1397
+ <div class="al-label"><span class="dot"></span> Document Layer</div>
1398
+ <div class="al-chips">
1399
+ <span class="al-chip">Document</span>
1400
+ <span class="al-chip">AstQuery</span>
1401
+ <span class="al-chip">NodeShortcuts</span>
1402
+ <span class="al-chip">Validator</span>
1403
+ <span class="al-chip">ModelInstance</span>
1404
+ </div>
1405
+ </div>
1406
+ <div class="arch-layer al--purple">
1407
+ <div class="al-label"><span class="dot"></span> Parsing Engine</div>
1408
+ <div class="al-chips">
1409
+ <span class="al-chip">unified</span>
1410
+ <span class="al-chip">remark-parse</span>
1411
+ <span class="al-chip">remark-gfm</span>
1412
+ <span class="al-chip">gray-matter</span>
1413
+ <span class="al-chip">MDAST</span>
1414
+ </div>
1415
+ </div>
1416
+ <div class="arch-layer al--cyan">
1417
+ <div class="al-label"><span class="dot"></span> File System</div>
1418
+ <div class="al-chips">
1419
+ <span class="al-chip">.md files</span>
1420
+ <span class="al-chip">.mdx files</span>
1421
+ <span class="al-chip">YAML frontmatter</span>
1422
+ <span class="al-chip">Folder hierarchy</span>
1423
+ </div>
1424
+ </div>
1425
+ </div>
1426
+
1427
+ <div class="arch-side" style="align-self: stretch; display: flex; flex-direction: column; justify-content: center;">
1428
+ <div style="font-family:var(--font-mono); font-size:10px; font-weight:600; text-transform:uppercase; letter-spacing:1.5px; color:var(--rose); margin-bottom:14px; display:flex; align-items:center; gap:8px;">
1429
+ <span style="width:8px;height:8px;border-radius:50%;background:var(--rose);display:inline-block;"></span>
1430
+ CLI Commands
1431
+ </div>
1432
+ <div style="display:flex; flex-direction:column; gap:6px;">
1433
+ <div class="cli-cmd"><span class="cmd-name">init</span> <span class="cmd-desc">scaffold project</span></div>
1434
+ <div class="cli-cmd"><span class="cmd-name">inspect</span> <span class="cmd-desc">show models</span></div>
1435
+ <div class="cli-cmd"><span class="cmd-name">validate</span> <span class="cmd-desc">check schemas</span></div>
1436
+ <div class="cli-cmd"><span class="cmd-name">create</span> <span class="cmd-desc">new document</span></div>
1437
+ <div class="cli-cmd"><span class="cmd-name">export</span> <span class="cmd-desc">JSON output</span></div>
1438
+ <div class="cli-cmd"><span class="cmd-name">extract</span> <span class="cmd-desc">filter sections</span></div>
1439
+ <div class="cli-cmd"><span class="cmd-name">summary</span> <span class="cmd-desc">gen docs</span></div>
1440
+ <div class="cli-cmd"><span class="cmd-name">teach</span> <span class="cmd-desc">LLM bundle</span></div>
1441
+ <div class="cli-cmd"><span class="cmd-name">action</span> <span class="cmd-desc">custom ops</span></div>
1442
+ </div>
1443
+ </div>
1444
+ </div>
1445
+
1446
+ <!-- ===== S6: CLI ===== -->
1447
+ <div id="s6" class="sec-head sec-head--blue anim-up" style="--i:22"><span class="num">6</span> CLI Toolkit</div>
1448
+
1449
+ <div class="code-block anim-up" style="--i:23">
1450
+ <div class="cb-header">
1451
+ <span class="dot" style="background:var(--blue)"></span>
1452
+ Terminal
1453
+ </div>
1454
+ <pre><span class="cm"># Scaffold a new project</span>
1455
+ <span class="fn">$</span> cbase init my-content
1456
+
1457
+ <span class="cm"># Create a new document from model schema</span>
1458
+ <span class="fn">$</span> cbase create Story --title <span class="str">"User can logout"</span>
1459
+
1460
+ <span class="cm"># Validate all documents against their model schemas</span>
1461
+ <span class="fn">$</span> cbase validate
1462
+ <span class="green" style="color:var(--green)"> ✓ epics/authentication — valid</span>
1463
+ <span class="green" style="color:var(--green)"> ✓ stories/authentication/user-can-register — valid</span>
1464
+
1465
+ <span class="cm"># Extract specific sections from matching documents</span>
1466
+ <span class="fn">$</span> cbase extract <span class="str">"stories/**/*"</span> --sections <span class="str">"Acceptance Criteria"</span>
1467
+
1468
+ <span class="cm"># Inspect model definitions</span>
1469
+ <span class="fn">$</span> cbase inspect
1470
+ <span class="tp"> Epic</span> prefix: epics/ 2 documents 1 relationship
1471
+ <span class="tp"> Story</span> prefix: stories/ 3 documents 2 sections
1472
+
1473
+ <span class="cm"># Generate documentation</span>
1474
+ <span class="fn">$</span> cbase summary <span class="cm"># → MODELS.md + TABLE-OF-CONTENTS.md</span>
1475
+ <span class="fn">$</span> cbase teach <span class="cm"># → Combined LLM-friendly bundle</span></pre>
1476
+ </div>
1477
+
1478
+ <div class="callout anim-up" style="--i:24">
1479
+ <strong>3-Tier Model Discovery:</strong> The CLI automatically finds your models via <code style="font-family:var(--font-mono);font-size:11px;background:var(--cyan-dim);color:var(--cyan);padding:1px 5px;border-radius:3px;">index.ts</code> exports, a standalone <code style="font-family:var(--font-mono);font-size:11px;background:var(--cyan-dim);color:var(--cyan);padding:1px 5px;border-radius:3px;">models.ts</code>, or auto-discovers from folder structure. Zero config needed to get started.
1480
+ </div>
1481
+
1482
+ <!-- ===== S7: SERVER APIs ===== -->
1483
+ <div id="s7" class="sec-head sec-head--purple anim-up" style="--i:25"><span class="num">7</span> Server APIs</div>
1484
+
1485
+ <div class="server-hero anim-up" style="--i:26">
1486
+ <div class="server-hero-inner">
1487
+ <div class="sh-badge">Coming Soon</div>
1488
+ <div class="sh-title">Every collection becomes an API. Automatically.</div>
1489
+ <div class="sh-subtitle">
1490
+ Define your models once &mdash; get a REST API, GraphQL API, and MCP server for free.
1491
+ Your markdown content becomes instantly queryable by custom apps, dashboards, and AI agents.
1492
+ </div>
1493
+
1494
+ <div class="api-cards">
1495
+ <div class="api-card api-card--cyan anim-scale" style="--i:27">
1496
+ <div class="ac-icon">
1497
+ <svg width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
1498
+ </div>
1499
+ <div class="ac-title">REST API</div>
1500
+ <div class="ac-desc">Full CRUD endpoints generated from your model definitions. Filter, paginate, and mutate content over HTTP.</div>
1501
+ <ul class="ac-endpoints">
1502
+ <li><code>GET</code> /api/stories?status=draft</li>
1503
+ <li><code>GET</code> /api/stories/:id/sections</li>
1504
+ <li><code>POST</code> /api/stories with validation</li>
1505
+ <li><code>GET</code> /api/epics/:id/relationships</li>
1506
+ </ul>
1507
+ </div>
1508
+
1509
+ <div class="api-card api-card--purple anim-scale" style="--i:28">
1510
+ <div class="ac-icon">
1511
+ <svg width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>
1512
+ </div>
1513
+ <div class="ac-title">GraphQL API</div>
1514
+ <div class="ac-desc">Schema auto-generated from Zod models. Query exactly the fields you need with full relationship traversal.</div>
1515
+ <ul class="ac-endpoints">
1516
+ <li>Types generated from Zod schemas</li>
1517
+ <li>Nested relationship queries</li>
1518
+ <li>Section extraction as fields</li>
1519
+ <li>Filter + sort built in</li>
1520
+ </ul>
1521
+ </div>
1522
+
1523
+ <div class="api-card api-card--green anim-scale" style="--i:29">
1524
+ <div class="ac-icon">
1525
+ <svg width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
1526
+ </div>
1527
+ <div class="ac-title">MCP Server</div>
1528
+ <div class="ac-desc">Expose your content to AI agents via Model Context Protocol. Claude, GPT, and any MCP client can query and update your docs.</div>
1529
+ <ul class="ac-endpoints">
1530
+ <li>Tools for query, read, create, update</li>
1531
+ <li>Resources for each model type</li>
1532
+ <li>Schema-aware content generation</li>
1533
+ <li>AI-native content workflows</li>
1534
+ </ul>
1535
+ </div>
1536
+ </div>
1537
+
1538
+ <div class="server-tagline">
1539
+ <strong>One command.</strong> &nbsp;<code style="font-family:var(--font-mono);font-size:12px;background:var(--surface2);color:var(--cyan);padding:3px 10px;border-radius:4px;border:1px solid var(--border);">cbase serve</code>&nbsp; spins up all three. Zero config.
1540
+ </div>
1541
+ </div>
1542
+ </div>
1543
+
1544
+ <!-- ===== S8: USE CASES ===== -->
1545
+ <div id="s8" class="sec-head sec-head--green anim-up" style="--i:30"><span class="num">8</span> Use Cases</div>
1546
+
1547
+ <div class="use-case-grid">
1548
+ <div class="use-case anim-up" style="--i:31">
1549
+ <div class="uc-icon">
1550
+ <svg width="28" height="28" fill="none" stroke="var(--cyan)" stroke-width="1.5" viewBox="0 0 24 24"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 11v6M9 14h6"/></svg>
1551
+ </div>
1552
+ <div class="uc-title">Project Specs & SDLCs</div>
1553
+ <div class="uc-desc">Epics, stories, and tasks as markdown files in Git. Query status, validate completeness, extract acceptance criteria for sprint planning.</div>
1554
+ </div>
1555
+ <div class="use-case anim-up" style="--i:32">
1556
+ <div class="uc-icon">
1557
+ <svg width="28" height="28" fill="none" stroke="var(--purple)" stroke-width="1.5" viewBox="0 0 24 24"><path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
1558
+ </div>
1559
+ <div class="uc-title">Documentation Sites</div>
1560
+ <div class="uc-desc">Typed content models for blog posts, API docs, changelogs. Schema validation catches missing fields before build.</div>
1561
+ </div>
1562
+ <div class="use-case anim-up" style="--i:33">
1563
+ <div class="uc-icon">
1564
+ <svg width="28" height="28" fill="none" stroke="var(--green)" stroke-width="1.5" viewBox="0 0 24 24"><path d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/></svg>
1565
+ </div>
1566
+ <div class="uc-title">Knowledge Bases</div>
1567
+ <div class="uc-desc">Queryable structure over runbooks, decision records, design docs. Search by tags, filter by status, traverse cross-references.</div>
1568
+ </div>
1569
+ <div class="use-case anim-up" style="--i:34">
1570
+ <div class="uc-icon">
1571
+ <svg width="28" height="28" fill="none" stroke="var(--amber)" stroke-width="1.5" viewBox="0 0 24 24"><path d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
1572
+ </div>
1573
+ <div class="uc-title">LLM Context Pipelines</div>
1574
+ <div class="uc-desc">Extract and combine document sections for LLM prompts. The <code style="font-family:var(--font-mono);font-size:10px;background:var(--amber-dim);color:var(--amber);padding:1px 5px;border-radius:3px;">teach</code> and <code style="font-family:var(--font-mono);font-size:10px;background:var(--amber-dim);color:var(--amber);padding:1px 5px;border-radius:3px;">extract</code> commands bundle content intelligently.</div>
1575
+ </div>
1576
+ </div>
1577
+
1578
+ <!-- ===== FOOTER ===== -->
1579
+ <div class="footer anim-up" style="--i:35">
1580
+ <div style="margin-bottom:8px;">
1581
+ <span style="color:var(--cyan)">contentbase</span> &middot; npm install contentbase
1582
+ </div>
1583
+ <div>An ORM for Markdown &mdash; by <a href="#">@soederpop</a></div>
1584
+ </div>
1585
+
1586
+ </div><!-- /main -->
1587
+ </div><!-- /wrap -->
1588
+
1589
+ <!-- Scroll spy -->
1590
+ <script>
1591
+ (function() {
1592
+ const toc = document.getElementById('toc');
1593
+ const links = toc.querySelectorAll('a');
1594
+ const sections = [];
1595
+
1596
+ links.forEach(link => {
1597
+ const id = link.getAttribute('href').slice(1);
1598
+ const el = document.getElementById(id);
1599
+ if (el) sections.push({ id, el, link });
1600
+ });
1601
+
1602
+ const observer = new IntersectionObserver(entries => {
1603
+ entries.forEach(entry => {
1604
+ if (entry.isIntersecting) {
1605
+ links.forEach(l => l.classList.remove('active'));
1606
+ const match = sections.find(s => s.el === entry.target);
1607
+ if (match) {
1608
+ match.link.classList.add('active');
1609
+ if (window.innerWidth <= 1000) {
1610
+ match.link.scrollIntoView({
1611
+ behavior: 'smooth', block: 'nearest', inline: 'center'
1612
+ });
1613
+ }
1614
+ }
1615
+ }
1616
+ });
1617
+ }, { rootMargin: '-10% 0px -80% 0px' });
1618
+
1619
+ sections.forEach(s => observer.observe(s.el));
1620
+
1621
+ links.forEach(link => {
1622
+ link.addEventListener('click', e => {
1623
+ e.preventDefault();
1624
+ const id = link.getAttribute('href').slice(1);
1625
+ const el = document.getElementById(id);
1626
+ if (el) {
1627
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' });
1628
+ history.replaceState(null, '', '#' + id);
1629
+ }
1630
+ });
1631
+ });
1632
+ })();
1633
+ </script>
1634
+
1635
+ </body>
1636
+ </html>