kitfly 0.1.2 → 0.2.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/CHANGELOG.md +46 -0
- package/README.md +63 -16
- package/VERSION +1 -1
- package/dist/_raw/content/deployment/preflight.md +134 -0
- package/dist/_raw/content/deployment/recipes/aws-s3.md +128 -0
- package/dist/_raw/content/deployment/recipes/cloudflare-pages.md +73 -0
- package/dist/_raw/content/deployment/recipes/cloudflare-r2.md +156 -0
- package/dist/_raw/content/deployment/recipes/fly-io.md +57 -0
- package/dist/_raw/content/deployment/recipes/github-pages.md +112 -0
- package/dist/_raw/content/deployment/recipes/netlify.md +99 -0
- package/dist/_raw/content/deployment/recipes/vercel.md +88 -0
- package/dist/_raw/content/deployment/secrets-and-env-vars.md +75 -0
- package/dist/_raw/content/deployment.md +128 -0
- package/dist/_raw/content/guide/approaches.md +182 -0
- package/dist/_raw/content/guide/features.md +121 -0
- package/dist/_raw/content/guide/getting-started.md +112 -0
- package/dist/_raw/content/guide/kitfly-overview.md +209 -0
- package/dist/_raw/content/reference/configuration.md +259 -0
- package/dist/_raw/content/reference/design-catalog.md +167 -0
- package/dist/_raw/content/reference/environment-variables.md +66 -0
- package/dist/_raw/content/reference/glossary.md +92 -0
- package/dist/_raw/content/reference/key-concepts.md +118 -0
- package/dist/_raw/content/reference/plugins.md +220 -0
- package/dist/_raw/content/reference/slides-authoring-guidelines.md +129 -0
- package/dist/_raw/content/reference/structure.md +166 -0
- package/dist/_raw/content/reference.md +20 -0
- package/dist/_raw/content/templates/crucible.md +192 -0
- package/dist/_raw/content/templates/handbook.md +83 -0
- package/dist/_raw/content/templates/minimal.md +138 -0
- package/dist/_raw/content/templates/overview.md +187 -0
- package/dist/_raw/content/templates/pipeline.md +151 -0
- package/dist/_raw/content/templates/productbook.md +187 -0
- package/dist/_raw/content/templates/runbook.md +193 -0
- package/dist/_raw/content/templates/servicebook.md +163 -0
- package/dist/_raw/docs/decisions/ADR-0001-minimalist-site-code.md +118 -0
- package/dist/_raw/docs/decisions/ADR-0002-ai-accessibility.md +153 -0
- package/dist/_raw/docs/decisions/ADR-0003-single-file-bundle.md +93 -0
- package/dist/_raw/docs/decisions/ADR-0004-bun-runtime.md +98 -0
- package/dist/_raw/docs/decisions/ADR-0005-plugin-contract-and-distribution.md +110 -0
- package/dist/_raw/docs/decisions/DDR-0001-viewport-locked-layout.md +111 -0
- package/dist/_raw/docs/decisions/DDR-0002-theme-system.md +131 -0
- package/dist/_raw/docs/decisions/DDR-0003-bounded-logo-slot.md +106 -0
- package/dist/_raw/docs/decisions/DDR-0004-slides-rendering-model.md +113 -0
- package/dist/_raw/docs/decisions/DDR-0005-deterministic-layout-boundary.md +107 -0
- package/dist/_raw/docs/userguide/cli/build.md +85 -0
- package/dist/_raw/docs/userguide/cli/bundle.md +81 -0
- package/dist/_raw/docs/userguide/cli/dev.md +92 -0
- package/dist/_raw/docs/userguide/cli/init.md +116 -0
- package/dist/_raw/docs/userguide/cli/servers.md +69 -0
- package/dist/_raw/docs/userguide/cli/stop.md +76 -0
- package/dist/_raw/docs/userguide/cli/update.md +78 -0
- package/dist/_raw/docs/userguide/cli/version.md +65 -0
- package/dist/_raw/docs/userguide/cli.md +34 -0
- package/dist/_raw/docs/userguide/sharing.md +94 -0
- package/dist/_raw/schemas/plugin-schemas-notes.md +71 -0
- package/dist/_raw/schemas.md +42 -0
- package/dist/assets/brand/kitfly-favicon-32.png +0 -0
- package/dist/assets/brand/kitfly-icon-64.png +0 -0
- package/dist/assets/brand/kitfly-logo-128.png +0 -0
- package/dist/assets/brand/kitfly-logo-512.png +0 -0
- package/dist/assets/brand/kitfly-logo.svg +12132 -0
- package/dist/assets/brand/kitfly-neon-128.png +0 -0
- package/dist/assets/brand/kitfly-neon-192.png +0 -0
- package/dist/assets/brand/kitfly-neon-256.png +0 -0
- package/dist/assets/brand/kitfly-neon.png +0 -0
- package/dist/assets/brand/palette.md +75 -0
- package/dist/content/deployment/index.html +11 -0
- package/dist/content/deployment/preflight.html +418 -0
- package/dist/content/deployment/recipes/aws-s3.html +421 -0
- package/dist/content/deployment/recipes/cloudflare-pages.html +372 -0
- package/dist/content/deployment/recipes/cloudflare-r2.html +443 -0
- package/dist/content/deployment/recipes/fly-io.html +356 -0
- package/dist/content/deployment/recipes/github-pages.html +414 -0
- package/dist/content/deployment/recipes/index.html +11 -0
- package/dist/content/deployment/recipes/netlify.html +394 -0
- package/dist/content/deployment/recipes/vercel.html +382 -0
- package/dist/content/deployment/secrets-and-env-vars.html +380 -0
- package/dist/content/deployment.html +426 -0
- package/dist/content/guide/approaches.html +501 -0
- package/dist/content/guide/features.html +436 -0
- package/dist/content/guide/getting-started.html +403 -0
- package/dist/content/guide/index.html +11 -0
- package/dist/content/guide/kitfly-overview.html +544 -0
- package/dist/content/index.html +11 -0
- package/dist/content/reference/configuration.html +580 -0
- package/dist/content/reference/design-catalog.html +449 -0
- package/dist/content/reference/environment-variables.html +367 -0
- package/dist/content/reference/glossary.html +368 -0
- package/dist/content/reference/index.html +11 -0
- package/dist/content/reference/key-concepts.html +399 -0
- package/dist/content/reference/plugins.html +491 -0
- package/dist/content/reference/slides-authoring-guidelines.html +418 -0
- package/dist/content/reference/structure.html +463 -0
- package/dist/content/reference.html +335 -0
- package/dist/content/templates/crucible.html +546 -0
- package/dist/content/templates/handbook.html +405 -0
- package/dist/content/templates/index.html +11 -0
- package/dist/content/templates/minimal.html +447 -0
- package/dist/content/templates/overview.html +558 -0
- package/dist/content/templates/pipeline.html +494 -0
- package/dist/content/templates/productbook.html +540 -0
- package/dist/content/templates/runbook.html +543 -0
- package/dist/content/templates/servicebook.html +523 -0
- package/dist/content-index.json +549 -0
- package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +491 -0
- package/dist/docs/decisions/ADR-0002-ai-accessibility.html +434 -0
- package/dist/docs/decisions/ADR-0003-single-file-bundle.html +412 -0
- package/dist/docs/decisions/ADR-0004-bun-runtime.html +409 -0
- package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +402 -0
- package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +459 -0
- package/dist/docs/decisions/DDR-0002-theme-system.html +452 -0
- package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +423 -0
- package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +399 -0
- package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +422 -0
- package/dist/docs/decisions/index.html +11 -0
- package/dist/docs/userguide/cli/build.html +408 -0
- package/dist/docs/userguide/cli/bundle.html +419 -0
- package/dist/docs/userguide/cli/dev.html +428 -0
- package/dist/docs/userguide/cli/index.html +11 -0
- package/dist/docs/userguide/cli/init.html +436 -0
- package/dist/docs/userguide/cli/servers.html +393 -0
- package/dist/docs/userguide/cli/stop.html +408 -0
- package/dist/docs/userguide/cli/update.html +406 -0
- package/dist/docs/userguide/cli/version.html +406 -0
- package/dist/docs/userguide/cli.html +386 -0
- package/dist/docs/userguide/index.html +11 -0
- package/dist/docs/userguide/sharing.html +465 -0
- package/dist/index.html +387 -0
- package/dist/llms.txt +18 -0
- package/dist/provenance.json +7 -0
- package/dist/schemas/index.html +11 -0
- package/dist/schemas/plugin-registry.schema.html +327 -0
- package/dist/schemas/plugin-schemas-notes.html +364 -0
- package/dist/schemas/plugin.schema.html +327 -0
- package/dist/schemas/plugins.schema.html +327 -0
- package/dist/schemas/v0/common.schema.html +386 -0
- package/dist/schemas/v0/index.html +11 -0
- package/dist/schemas/v0/plugin-registry.schema.html +547 -0
- package/dist/schemas/v0/plugin.schema.html +497 -0
- package/dist/schemas/v0/plugins.schema.html +406 -0
- package/dist/schemas/v0/site.schema.html +541 -0
- package/dist/schemas/v0/theme.schema.html +615 -0
- package/dist/schemas.html +351 -0
- package/dist/styles.css +1262 -0
- package/package.json +4 -2
- package/plugins-dist/callouts.css +32 -0
- package/plugins-dist/callouts.js +46 -0
- package/plugins-dist/slides-visuals.css +390 -0
- package/plugins-dist/slides-visuals.js +689 -0
- package/registry/plugins.yaml +35 -0
- package/schemas/README.md +10 -0
- package/schemas/plugin-registry.schema.json +5 -0
- package/schemas/plugin-schemas-notes.md +71 -0
- package/schemas/plugin.schema.json +5 -0
- package/schemas/plugins.schema.json +5 -0
- package/schemas/v0/common.schema.json +64 -0
- package/schemas/v0/plugin-registry.schema.json +225 -0
- package/schemas/v0/plugin.schema.json +175 -0
- package/schemas/v0/plugins.schema.json +84 -0
- package/schemas/v0/site.schema.json +56 -9
- package/schemas/v0/theme.schema.json +105 -22
- package/scripts/build.ts +158 -3
- package/scripts/bundle.ts +261 -95
- package/scripts/dev.ts +301 -11
- package/src/__tests__/build.test.ts +220 -1
- package/src/__tests__/bundle.test.ts +31 -0
- package/src/__tests__/cli.test.ts +14 -3
- package/src/__tests__/dev-plugin-errors.test.ts +20 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/bad-list-indent.md +5 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/blank-line.md +5 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/compare-object-items.md +9 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/flow-branching-no-source.md +5 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/flow-converging-no-target.md +6 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/indented-fence.md +4 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/staircase-empty-steps.md +3 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/stat-grid-missing-fields.md +5 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/timeline-horizontal-no-events.md +2 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/unknown-type.md +3 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/compare.md +10 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/comparison-table.md +14 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-branching-no-split.md +7 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-branching.md +8 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-converging-no-merge.md +7 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-converging.md +8 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/funnel.md +7 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/kpi.md +5 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/layer-cake.md +6 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/pyramid.md +6 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/quadrant-grid.md +8 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/scorecard.md +13 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/staircase-down.md +7 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/staircase.md +8 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/stat-grid.md +8 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/timeline-horizontal.md +9 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/timeline-vertical.md +10 -0
- package/src/__tests__/init.test.ts +35 -0
- package/src/__tests__/plugin-loader.test.ts +221 -0
- package/src/__tests__/shared.test.ts +451 -0
- package/src/__tests__/slides-visuals-fence-contract.test.ts +28 -0
- package/src/__tests__/slides-visuals-runtime-regressions.bun.test.ts +147 -0
- package/src/__tests__/styles.test.ts +35 -0
- package/src/cli.ts +9 -4
- package/src/plugin-loader.ts +245 -0
- package/src/shared.ts +650 -7
- package/src/site/styles.css +331 -0
- package/src/site/template.html +66 -5
- package/src/templates/deck.ts +186 -0
- package/src/templates/driver.ts +11 -1
- package/src/templates/minimal.ts +1 -0
|
@@ -0,0 +1,443 @@
|
|
|
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>Recipe: Cloudflare R2 - Kitfly Docs</title>
|
|
7
|
+
<link rel="icon" type="image/png" sizes="32x32" href="../../../assets/brand/kitfly-favicon-32.png">
|
|
8
|
+
<link rel="icon" type="image/png" sizes="64x64" href="../../../assets/brand/kitfly-neon-256.png">
|
|
9
|
+
<link rel="stylesheet" href="../../../styles.css">
|
|
10
|
+
<style id="kitfly-theme">
|
|
11
|
+
:root { --color-bg: #ffffff;
|
|
12
|
+
--color-bg-sidebar: #f5f7f8;
|
|
13
|
+
--color-text: #374151;
|
|
14
|
+
--color-text-muted: #6b7280;
|
|
15
|
+
--color-border: #e5e7eb;
|
|
16
|
+
--color-link: #007182;
|
|
17
|
+
--color-link-hover: #0a6172;
|
|
18
|
+
--color-accent: #152F46;
|
|
19
|
+
--color-code-bg: #f5f7f8;
|
|
20
|
+
--color-logo: #152F46;
|
|
21
|
+
--sidebar-width: 280px;
|
|
22
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
23
|
+
--font-headings: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
24
|
+
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; }
|
|
25
|
+
html { font-size: 16px; }
|
|
26
|
+
@media (prefers-color-scheme: dark) {
|
|
27
|
+
:root:not([data-theme="light"]) { --color-bg: #0d1117;
|
|
28
|
+
--color-bg-sidebar: #152F46;
|
|
29
|
+
--color-text: #e5e7eb;
|
|
30
|
+
--color-text-muted: #9ca3af;
|
|
31
|
+
--color-border: #374151;
|
|
32
|
+
--color-link: #709EA6;
|
|
33
|
+
--color-link-hover: #8fb5bc;
|
|
34
|
+
--color-accent: #f9fafb;
|
|
35
|
+
--color-code-bg: #152F46;
|
|
36
|
+
--color-logo: #f9fafb; }
|
|
37
|
+
}
|
|
38
|
+
[data-theme="dark"] { --color-bg: #0d1117;
|
|
39
|
+
--color-bg-sidebar: #152F46;
|
|
40
|
+
--color-text: #e5e7eb;
|
|
41
|
+
--color-text-muted: #9ca3af;
|
|
42
|
+
--color-border: #374151;
|
|
43
|
+
--color-link: #709EA6;
|
|
44
|
+
--color-link-hover: #8fb5bc;
|
|
45
|
+
--color-accent: #f9fafb;
|
|
46
|
+
--color-code-bg: #152F46;
|
|
47
|
+
--color-logo: #f9fafb; }
|
|
48
|
+
[data-theme="light"] { --color-bg: #ffffff;
|
|
49
|
+
--color-bg-sidebar: #f5f7f8;
|
|
50
|
+
--color-text: #374151;
|
|
51
|
+
--color-text-muted: #6b7280;
|
|
52
|
+
--color-border: #e5e7eb;
|
|
53
|
+
--color-link: #007182;
|
|
54
|
+
--color-link-hover: #0a6172;
|
|
55
|
+
--color-accent: #152F46;
|
|
56
|
+
--color-code-bg: #f5f7f8;
|
|
57
|
+
--color-logo: #152F46;
|
|
58
|
+
--sidebar-width: 280px;
|
|
59
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
60
|
+
--font-headings: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
61
|
+
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; }
|
|
62
|
+
</style>
|
|
63
|
+
<!-- Syntax highlighting - Prism.js -->
|
|
64
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css" id="prism-light">
|
|
65
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-okaidia.min.css" id="prism-dark" disabled>
|
|
66
|
+
|
|
67
|
+
<script>
|
|
68
|
+
// Apply saved theme immediately to prevent flash
|
|
69
|
+
(function() {
|
|
70
|
+
const saved = localStorage.getItem('theme');
|
|
71
|
+
if (saved) {
|
|
72
|
+
document.documentElement.setAttribute('data-theme', saved);
|
|
73
|
+
}
|
|
74
|
+
// Set Prism theme based on saved or system preference
|
|
75
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
76
|
+
const isDark = saved === 'dark' || (!saved && prefersDark);
|
|
77
|
+
if (isDark) {
|
|
78
|
+
document.getElementById('prism-light')?.setAttribute('disabled', '');
|
|
79
|
+
document.getElementById('prism-dark')?.removeAttribute('disabled');
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
</script>
|
|
83
|
+
</head>
|
|
84
|
+
<body class="mode-docs">
|
|
85
|
+
<div class="mobile-header">
|
|
86
|
+
<button class="nav-toggle" onclick="toggleNav()" aria-label="Toggle navigation">
|
|
87
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
88
|
+
<path d="M3 12h18M3 6h18M3 18h18"/>
|
|
89
|
+
</svg>
|
|
90
|
+
</button>
|
|
91
|
+
<a href="../../../" class="mobile-logo" title="Home" data-initial="K">
|
|
92
|
+
<img src="../../../assets/brand/kitfly-neon-256.png" alt="Kitfly" class="logo-img logo-icon" onerror="this.onerror=null;this.style.display='none';this.parentElement.classList.add('logo-fallback')"/>
|
|
93
|
+
</a>
|
|
94
|
+
<button class="mobile-theme-toggle" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme">
|
|
95
|
+
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
96
|
+
<circle cx="12" cy="12" r="5"/>
|
|
97
|
+
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
|
98
|
+
</svg>
|
|
99
|
+
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
100
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
101
|
+
</svg>
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="layout">
|
|
105
|
+
<nav class="sidebar">
|
|
106
|
+
<div class="sidebar-header">
|
|
107
|
+
<div class="logo logo-icon">
|
|
108
|
+
<a href="/" class="logo-icon" data-initial="K">
|
|
109
|
+
<img src="../../../assets/brand/kitfly-neon-256.png" alt="Kitfly" class="logo-img" onerror="this.onerror=null;this.style.display='none';this.parentElement.classList.add('logo-fallback')"/>
|
|
110
|
+
</a>
|
|
111
|
+
<span class="logo-text">
|
|
112
|
+
<a href="/" class="brand">Kitfly</a>
|
|
113
|
+
<a href="../../../" class="product">Kitfly Docs</a>
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="header-tools">
|
|
117
|
+
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme">
|
|
118
|
+
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
119
|
+
<circle cx="12" cy="12" r="5"/>
|
|
120
|
+
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
|
121
|
+
</svg>
|
|
122
|
+
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
123
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
124
|
+
</svg>
|
|
125
|
+
</button>
|
|
126
|
+
<div class="sidebar-meta">
|
|
127
|
+
<span class="meta-version">v0.2.1</span>
|
|
128
|
+
<span class="meta-branch">HEAD</span>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="sidebar-nav">
|
|
133
|
+
<ul><li><a href="../../../index.html" class="nav-home">Home</a></li><li><span class="nav-section">Guide</span><ul><li><a href="../../../content/guide/approaches.html">approaches</a></li><li><a href="../../../content/guide/features.html">features</a></li><li><a href="../../../content/guide/getting-started.html">getting-started</a></li><li><a href="../../../content/guide/kitfly-overview.html">kitfly-overview</a></li></ul></li><li><span class="nav-section">Templates</span><ul><li><a href="../../../content/templates/crucible.html">crucible</a></li><li><a href="../../../content/templates/handbook.html">handbook</a></li><li><a href="../../../content/templates/minimal.html">minimal</a></li><li><a href="../../../content/templates/overview.html">overview</a></li><li><a href="../../../content/templates/pipeline.html">pipeline</a></li><li><a href="../../../content/templates/productbook.html">productbook</a></li><li><a href="../../../content/templates/runbook.html">runbook</a></li><li><a href="../../../content/templates/servicebook.html">servicebook</a></li></ul></li><li><a href="../../../content/reference.html" class="nav-section">Reference</a><ul><li><a href="../../../content/reference/configuration.html">configuration</a></li><li><a href="../../../content/reference/design-catalog.html">design-catalog</a></li><li><a href="../../../content/reference/environment-variables.html">environment-variables</a></li><li><a href="../../../content/reference/glossary.html">glossary</a></li><li><a href="../../../content/reference/key-concepts.html">key-concepts</a></li><li><a href="../../../content/reference/plugins.html">plugins</a></li><li><a href="../../../content/reference/slides-authoring-guidelines.html">slides-authoring-guidelines</a></li><li><a href="../../../content/reference/structure.html">structure</a></li></ul></li><li><a href="../../../content/deployment.html" class="nav-section">Deployment</a><ul><li><a href="../../../content/deployment/preflight.html">preflight</a></li><li><details open><summary class="nav-group">recipes</summary><ul><li><a href="../../../content/deployment/recipes/aws-s3.html">aws-s3</a></li><li><a href="../../../content/deployment/recipes/cloudflare-pages.html">cloudflare-pages</a></li><li><a href="../../../content/deployment/recipes/cloudflare-r2.html" class="active">cloudflare-r2</a></li><li><a href="../../../content/deployment/recipes/fly-io.html">fly-io</a></li><li><a href="../../../content/deployment/recipes/github-pages.html">github-pages</a></li><li><a href="../../../content/deployment/recipes/netlify.html">netlify</a></li><li><a href="../../../content/deployment/recipes/vercel.html">vercel</a></li></ul></details></li><li><a href="../../../content/deployment/secrets-and-env-vars.html">secrets-and-env-vars</a></li></ul></li><li><span class="nav-section">User Guide</span><ul><li><details><summary class="nav-group"><a href="../../../docs/userguide/cli.html">cli</a></summary><ul><li><a href="../../../docs/userguide/cli/build.html">build</a></li><li><a href="../../../docs/userguide/cli/bundle.html">bundle</a></li><li><a href="../../../docs/userguide/cli/dev.html">dev</a></li><li><a href="../../../docs/userguide/cli/init.html">init</a></li><li><a href="../../../docs/userguide/cli/servers.html">servers</a></li><li><a href="../../../docs/userguide/cli/stop.html">stop</a></li><li><a href="../../../docs/userguide/cli/update.html">update</a></li><li><a href="../../../docs/userguide/cli/version.html">version</a></li></ul></details></li><li><a href="../../../docs/userguide/sharing.html">sharing</a></li></ul></li><li><span class="nav-section">Decisions</span><ul><li><a href="../../../docs/decisions/ADR-0001-minimalist-site-code.html">ADR-0001-minimalist-site-code</a></li><li><a href="../../../docs/decisions/ADR-0002-ai-accessibility.html">ADR-0002-ai-accessibility</a></li><li><a href="../../../docs/decisions/ADR-0003-single-file-bundle.html">ADR-0003-single-file-bundle</a></li><li><a href="../../../docs/decisions/ADR-0004-bun-runtime.html">ADR-0004-bun-runtime</a></li><li><a href="../../../docs/decisions/ADR-0005-plugin-contract-and-distribution.html">ADR-0005-plugin-contract-and-distribution</a></li><li><a href="../../../docs/decisions/DDR-0001-viewport-locked-layout.html">DDR-0001-viewport-locked-layout</a></li><li><a href="../../../docs/decisions/DDR-0002-theme-system.html">DDR-0002-theme-system</a></li><li><a href="../../../docs/decisions/DDR-0003-bounded-logo-slot.html">DDR-0003-bounded-logo-slot</a></li><li><a href="../../../docs/decisions/DDR-0004-slides-rendering-model.html">DDR-0004-slides-rendering-model</a></li><li><a href="../../../docs/decisions/DDR-0005-deterministic-layout-boundary.html">DDR-0005-deterministic-layout-boundary</a></li></ul></li><li><a href="../../../schemas.html" class="nav-section">Schemas</a><ul><li><a href="../../../schemas/plugin-registry.schema.html">plugin-registry.schema</a></li><li><a href="../../../schemas/plugin-schemas-notes.html">plugin-schemas-notes</a></li><li><a href="../../../schemas/plugin.schema.html">plugin.schema</a></li><li><a href="../../../schemas/plugins.schema.html">plugins.schema</a></li><li><details><summary class="nav-group">v0</summary><ul><li><a href="../../../schemas/v0/common.schema.html">common.schema</a></li><li><a href="../../../schemas/v0/plugin-registry.schema.html">plugin-registry.schema</a></li><li><a href="../../../schemas/v0/plugin.schema.html">plugin.schema</a></li><li><a href="../../../schemas/v0/plugins.schema.html">plugins.schema</a></li><li><a href="../../../schemas/v0/site.schema.html">site.schema</a></li><li><a href="../../../schemas/v0/theme.schema.html">theme.schema</a></li></ul></details></li></ul></li></ul>
|
|
134
|
+
</div>
|
|
135
|
+
</nav>
|
|
136
|
+
<main class="content">
|
|
137
|
+
<article class="prose">
|
|
138
|
+
<nav class="breadcrumbs"><a href="../../../content/guide/approaches.html">Content</a><span class="separator">›</span><a href="../../../content/deployment/preflight.html">Deployment</a><span class="separator">›</span><a href="../../../content/deployment/recipes/aws-s3.html">Recipes</a><span class="separator">›</span><span>cloudflare-r2</span></nav>
|
|
139
|
+
<div class="page-meta">Last updated: 2026-02-12</div>
|
|
140
|
+
<h1 id="recipe-cloudflare-r2">Recipe: Cloudflare R2</h1>
|
|
141
|
+
<p>This recipe is intentionally labeled <strong>advanced</strong>.</p>
|
|
142
|
+
<p>If you want the simplest Cloudflare experience for a static site, Cloudflare Pages is usually the easiest path:</p>
|
|
143
|
+
<ul>
|
|
144
|
+
<li><a href="cloudflare-pages.html">Recipe: Cloudflare Pages</a></li>
|
|
145
|
+
</ul>
|
|
146
|
+
<p>R2 is great when you want object storage as your origin and you're comfortable wiring edge routing/caching.</p>
|
|
147
|
+
<h2 id="when-to-use-this">When to use this</h2>
|
|
148
|
+
<ul>
|
|
149
|
+
<li>You're already using Cloudflare (DNS, Workers, caching)</li>
|
|
150
|
+
<li>You want <code>dist/</code> stored as objects and served at the edge</li>
|
|
151
|
+
</ul>
|
|
152
|
+
<h2 id="pages-vs-r2-which-should-i-choose">Pages vs R2 (which should I choose?)</h2>
|
|
153
|
+
<h3 id="choose-cloudflare-pages-when">Choose Cloudflare Pages when…</h3>
|
|
154
|
+
<ul>
|
|
155
|
+
<li>You want a "deploy and host" product for static sites</li>
|
|
156
|
+
<li>You're fine with "connect a repo and publish <code>dist/</code>" as your workflow</li>
|
|
157
|
+
<li>You want custom domains + HTTPS without a lot of plumbing</li>
|
|
158
|
+
</ul>
|
|
159
|
+
<h3 id="choose-cloudflare-r2-when">Choose Cloudflare R2 when…</h3>
|
|
160
|
+
<ul>
|
|
161
|
+
<li>You specifically want object storage as the source of truth for site files</li>
|
|
162
|
+
<li>You already have (or want) a Worker/edge layer in front</li>
|
|
163
|
+
<li>You care about storing and serving lots of assets and want S3-like primitives</li>
|
|
164
|
+
</ul>
|
|
165
|
+
<h2 id="high-level-shape">High-level shape</h2>
|
|
166
|
+
<ol>
|
|
167
|
+
<li>Build your site to <code>dist/</code></li>
|
|
168
|
+
<li>Upload <code>dist/</code> to an R2 bucket</li>
|
|
169
|
+
<li>Serve it via an edge layer (typically a Worker) and attach your domain</li>
|
|
170
|
+
</ol>
|
|
171
|
+
<p>Important: R2 is object storage, not "a website host" by itself. You usually need something in front of it to handle requests and return the right file for a path.</p>
|
|
172
|
+
<h2 id="prerequisites">Prerequisites</h2>
|
|
173
|
+
<ul>
|
|
174
|
+
<li>A Cloudflare account</li>
|
|
175
|
+
<li>A domain on Cloudflare DNS (recommended)</li>
|
|
176
|
+
<li><code>wrangler</code> CLI installed: <code>npm i -g wrangler</code></li>
|
|
177
|
+
<li>An R2 bucket created (via dashboard or <code>wrangler r2 bucket create <name></code>)</li>
|
|
178
|
+
</ul>
|
|
179
|
+
<h2 id="build">Build</h2>
|
|
180
|
+
<pre><code class="language-bash">make build
|
|
181
|
+
</code></pre>
|
|
182
|
+
<h2 id="upload-dist-to-r2">Upload <code>dist/</code> to R2</h2>
|
|
183
|
+
<h3 id="using-wrangler-recommended-for-cicd">Using wrangler (recommended for CI/CD)</h3>
|
|
184
|
+
<p>Upload individual files:</p>
|
|
185
|
+
<pre><code class="language-bash"># Upload all files from dist/ to the bucket
|
|
186
|
+
for file in $(find dist -type f); do
|
|
187
|
+
key="${file#dist/}"
|
|
188
|
+
wrangler r2 object put "$KITFLY_CF_R2_BUCKET/$key" --file="$file"
|
|
189
|
+
done
|
|
190
|
+
</code></pre>
|
|
191
|
+
<p>Or if you prefer a sync-like approach, use the rclone tool with R2's S3-compatible endpoint:</p>
|
|
192
|
+
<pre><code class="language-bash">rclone sync dist/ "r2:$KITFLY_CF_R2_BUCKET/" \
|
|
193
|
+
--s3-provider=Cloudflare \
|
|
194
|
+
--s3-access-key-id="$KITFLY_CF_R2_ACCESS_KEY_ID" \
|
|
195
|
+
--s3-secret-access-key="$KITFLY_CF_R2_SECRET_ACCESS_KEY" \
|
|
196
|
+
--s3-endpoint="https://$KITFLY_CF_R2_ACCOUNT_ID.r2.cloudflarestorage.com"
|
|
197
|
+
</code></pre>
|
|
198
|
+
<h3 id="using-the-dashboard">Using the dashboard</h3>
|
|
199
|
+
<p>Fine for small sites, but manual. Upload files in the R2 bucket view.</p>
|
|
200
|
+
<h2 id="serving-worker-in-front-of-r2">Serving: Worker in front of R2</h2>
|
|
201
|
+
<p>A minimal Worker to serve files from R2:</p>
|
|
202
|
+
<pre><code class="language-javascript">// wrangler.toml:
|
|
203
|
+
// [[r2_buckets]]
|
|
204
|
+
// binding = "BUCKET"
|
|
205
|
+
// bucket_name = "your-bucket-name"
|
|
206
|
+
|
|
207
|
+
export default {
|
|
208
|
+
async fetch(request, env) {
|
|
209
|
+
const url = new URL(request.url);
|
|
210
|
+
let key = url.pathname.slice(1) || "index.html";
|
|
211
|
+
if (key.endsWith("/")) key += "index.html";
|
|
212
|
+
|
|
213
|
+
const object = await env.BUCKET.get(key);
|
|
214
|
+
if (!object) return new Response("Not found", { status: 404 });
|
|
215
|
+
|
|
216
|
+
const headers = new Headers();
|
|
217
|
+
headers.set(
|
|
218
|
+
"Content-Type",
|
|
219
|
+
object.httpMetadata?.contentType || "text/html",
|
|
220
|
+
);
|
|
221
|
+
return new Response(object.body, { headers });
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
</code></pre>
|
|
225
|
+
<p>Deploy the Worker with <code>wrangler deploy</code> and attach your custom domain via the Cloudflare dashboard.</p>
|
|
226
|
+
<h2 id="secrets">Secrets</h2>
|
|
227
|
+
<p>Keep R2 credentials in environment variables and <strong>don't</strong> commit them. See:</p>
|
|
228
|
+
<ul>
|
|
229
|
+
<li><a href="../secrets-and-env-vars.html">Secrets and Environment Variables</a></li>
|
|
230
|
+
</ul>
|
|
231
|
+
<h2 id="caching-dont-fight-it">Caching (don't fight it)</h2>
|
|
232
|
+
<p>Cloudflare will cache aggressively if you ask it to. That's great for docs sites, but it can confuse first-time deploys.</p>
|
|
233
|
+
<p>If you deploy and still see old content:</p>
|
|
234
|
+
<ul>
|
|
235
|
+
<li>hard refresh (Shift+Reload)</li>
|
|
236
|
+
<li>try an incognito window</li>
|
|
237
|
+
<li>wait a minute</li>
|
|
238
|
+
<li>or purge cache via the Cloudflare dashboard / API (see <a href="../preflight.html">Preflight: Cache Invalidation</a>)</li>
|
|
239
|
+
</ul>
|
|
240
|
+
<h2 id="dns-basics">DNS basics</h2>
|
|
241
|
+
<p>Typical pattern:</p>
|
|
242
|
+
<ul>
|
|
243
|
+
<li>Use a <strong>subdomain</strong> like <code>docs.example.com</code></li>
|
|
244
|
+
<li>Point it at your Worker using Cloudflare's "Custom Domains for Workers" feature</li>
|
|
245
|
+
</ul>
|
|
246
|
+
<p>HTTPS is handled automatically when using Cloudflare DNS.</p>
|
|
247
|
+
<h2 id="verify">Verify</h2>
|
|
248
|
+
<ul>
|
|
249
|
+
<li>Load the site from your custom domain</li>
|
|
250
|
+
<li>Hard refresh once (caching can make you think deploy didn't work)</li>
|
|
251
|
+
<li>Click 3–5 pages and confirm assets load</li>
|
|
252
|
+
<li>Confirm the HTTPS lock icon</li>
|
|
253
|
+
</ul>
|
|
254
|
+
<h2 id="rollback">Rollback</h2>
|
|
255
|
+
<p>Rollback options:</p>
|
|
256
|
+
<ul>
|
|
257
|
+
<li>re-upload the previous <code>dist/</code> objects</li>
|
|
258
|
+
<li>or switch your Worker routing back to a prior bucket/prefix</li>
|
|
259
|
+
</ul>
|
|
260
|
+
<p>See: <a href="../preflight.html">Preflight and Rollback</a></p>
|
|
261
|
+
|
|
262
|
+
</article>
|
|
263
|
+
<aside class="toc"><span class="toc-title">On this page</span><ul><li><a href="#when-to-use-this">When to use this</a></li><li><a href="#pages-vs-r2-which-should-i-choose">Pages vs R2 (which should I choose?)</a></li><li class="toc-h3"><a href="#choose-cloudflare-pages-when">Choose Cloudflare Pages when…</a></li><li class="toc-h3"><a href="#choose-cloudflare-r2-when">Choose Cloudflare R2 when…</a></li><li><a href="#high-level-shape">High-level shape</a></li><li><a href="#prerequisites">Prerequisites</a></li><li><a href="#build">Build</a></li><li class="toc-h3"><a href="#using-wrangler-recommended-for-cicd">Using wrangler (recommended for CI/CD)</a></li><li class="toc-h3"><a href="#using-the-dashboard">Using the dashboard</a></li><li><a href="#serving-worker-in-front-of-r2">Serving: Worker in front of R2</a></li><li><a href="#secrets">Secrets</a></li><li><a href="#caching-dont-fight-it">Caching (don't fight it)</a></li><li><a href="#dns-basics">DNS basics</a></li><li><a href="#verify">Verify</a></li><li><a href="#rollback">Rollback</a></li></ul></aside>
|
|
264
|
+
</main>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<footer class="site-footer">
|
|
268
|
+
<div class="footer-content">
|
|
269
|
+
<div class="footer-left">
|
|
270
|
+
<span class="footer-version">v0.2.1</span>
|
|
271
|
+
<span class="footer-separator">·</span>
|
|
272
|
+
<span class="footer-commit" title="Commit: 30dfc01">Published 2026-02-15</span>
|
|
273
|
+
</div>
|
|
274
|
+
<div class="footer-center">
|
|
275
|
+
<span class="footer-copyright"><a href="https://3leaps.net" class="footer-link">© 2026 3 Leaps, LLC</a></span>
|
|
276
|
+
<span class="footer-separator">·</span><a href="/" class="footer-link">Kitfly</a>
|
|
277
|
+
</div>
|
|
278
|
+
<div class="footer-right">
|
|
279
|
+
<a href="https://kitfly.dev" class="footer-link">Built with Kitfly</a>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</footer>
|
|
283
|
+
<!-- Syntax highlighting - Prism.js -->
|
|
284
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js"></script>
|
|
285
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js"></script>
|
|
286
|
+
<!-- Mermaid diagram support -->
|
|
287
|
+
<script type="module">
|
|
288
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
289
|
+
|
|
290
|
+
function getMermaidTheme() {
|
|
291
|
+
const theme = document.documentElement.getAttribute('data-theme');
|
|
292
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
293
|
+
const isDark = theme === 'dark' || (!theme && prefersDark);
|
|
294
|
+
return isDark ? 'dark' : 'neutral';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
mermaid.initialize({
|
|
298
|
+
startOnLoad: true,
|
|
299
|
+
theme: getMermaidTheme()
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Re-render mermaid diagrams when theme changes
|
|
303
|
+
window.reinitMermaid = async function() {
|
|
304
|
+
mermaid.initialize({ startOnLoad: false, theme: getMermaidTheme() });
|
|
305
|
+
const diagrams = document.querySelectorAll('.mermaid');
|
|
306
|
+
for (const el of diagrams) {
|
|
307
|
+
const code = el.getAttribute('data-mermaid-source');
|
|
308
|
+
if (code) {
|
|
309
|
+
el.innerHTML = code;
|
|
310
|
+
el.removeAttribute('data-processed');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
await mermaid.run({ nodes: diagrams });
|
|
314
|
+
};
|
|
315
|
+
</script>
|
|
316
|
+
|
|
317
|
+
<script>
|
|
318
|
+
function toggleTheme() {
|
|
319
|
+
const html = document.documentElement;
|
|
320
|
+
const current = html.getAttribute('data-theme');
|
|
321
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
322
|
+
|
|
323
|
+
let next;
|
|
324
|
+
if (current === 'dark') {
|
|
325
|
+
next = 'light';
|
|
326
|
+
} else if (current === 'light') {
|
|
327
|
+
next = 'dark';
|
|
328
|
+
} else {
|
|
329
|
+
// No explicit theme set, toggle from system preference
|
|
330
|
+
next = prefersDark ? 'light' : 'dark';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
html.setAttribute('data-theme', next);
|
|
334
|
+
localStorage.setItem('theme', next);
|
|
335
|
+
|
|
336
|
+
// Switch Prism theme
|
|
337
|
+
const prismLight = document.getElementById('prism-light');
|
|
338
|
+
const prismDark = document.getElementById('prism-dark');
|
|
339
|
+
if (next === 'dark') {
|
|
340
|
+
prismLight?.setAttribute('disabled', '');
|
|
341
|
+
prismDark?.removeAttribute('disabled');
|
|
342
|
+
} else {
|
|
343
|
+
prismLight?.removeAttribute('disabled');
|
|
344
|
+
prismDark?.setAttribute('disabled', '');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Re-render mermaid diagrams with new theme
|
|
348
|
+
if (window.reinitMermaid) {
|
|
349
|
+
window.reinitMermaid();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Slides mode hash routing
|
|
354
|
+
(function initSlidesMode() {
|
|
355
|
+
const shell = document.querySelector('.slides-shell');
|
|
356
|
+
if (!shell) return;
|
|
357
|
+
|
|
358
|
+
const slides = Array.from(document.querySelectorAll('.slide'));
|
|
359
|
+
if (!slides.length) return;
|
|
360
|
+
|
|
361
|
+
const prevBtn = document.querySelector('.slide-prev');
|
|
362
|
+
const nextBtn = document.querySelector('.slide-next');
|
|
363
|
+
const counter = document.querySelector('.slide-counter');
|
|
364
|
+
const progressBar = document.querySelector('.slide-progress-bar');
|
|
365
|
+
const navLinks = Array.from(document.querySelectorAll('.sidebar-nav a[href^="#slide-"]'));
|
|
366
|
+
let current = 0;
|
|
367
|
+
|
|
368
|
+
function setActive(n) {
|
|
369
|
+
current = Math.max(0, Math.min(n, slides.length - 1));
|
|
370
|
+
slides.forEach((slide, idx) => slide.classList.toggle('active', idx === current));
|
|
371
|
+
navLinks.forEach((link) => {
|
|
372
|
+
const active = link.getAttribute('href') === '#' + slides[current].id;
|
|
373
|
+
link.classList.toggle('active', active);
|
|
374
|
+
});
|
|
375
|
+
if (counter) counter.textContent = (current + 1) + ' / ' + slides.length;
|
|
376
|
+
if (progressBar) progressBar.style.width = (((current + 1) / slides.length) * 100) + '%';
|
|
377
|
+
if (prevBtn) prevBtn.disabled = current === 0;
|
|
378
|
+
if (nextBtn) nextBtn.disabled = current === slides.length - 1;
|
|
379
|
+
history.replaceState(null, '', '#' + slides[current].id);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function setFromHash() {
|
|
383
|
+
const hash = window.location.hash || '';
|
|
384
|
+
const idx = slides.findIndex((s) => '#' + s.id === hash);
|
|
385
|
+
if (idx >= 0) setActive(idx);
|
|
386
|
+
else setActive(0);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
prevBtn?.addEventListener('click', () => setActive(current - 1));
|
|
390
|
+
nextBtn?.addEventListener('click', () => setActive(current + 1));
|
|
391
|
+
|
|
392
|
+
document.addEventListener('keydown', (e) => {
|
|
393
|
+
if (e.key === 'ArrowRight' || e.key === ' ') {
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
setActive(current + 1);
|
|
396
|
+
} else if (e.key === 'ArrowLeft') {
|
|
397
|
+
e.preventDefault();
|
|
398
|
+
setActive(current - 1);
|
|
399
|
+
} else if (e.key === 'Home') {
|
|
400
|
+
e.preventDefault();
|
|
401
|
+
setActive(0);
|
|
402
|
+
} else if (e.key === 'End') {
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
setActive(slides.length - 1);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
window.addEventListener('hashchange', setFromHash);
|
|
409
|
+
setFromHash();
|
|
410
|
+
})();
|
|
411
|
+
|
|
412
|
+
// Copy code button
|
|
413
|
+
document.querySelectorAll('.prose pre code').forEach(block => {
|
|
414
|
+
const button = document.createElement('button');
|
|
415
|
+
button.className = 'copy-button';
|
|
416
|
+
button.textContent = 'Copy';
|
|
417
|
+
button.onclick = async () => {
|
|
418
|
+
await navigator.clipboard.writeText(block.textContent);
|
|
419
|
+
button.textContent = 'Copied!';
|
|
420
|
+
setTimeout(() => button.textContent = 'Copy', 2000);
|
|
421
|
+
};
|
|
422
|
+
block.parentElement.appendChild(button);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Mobile nav toggle
|
|
426
|
+
function toggleNav() {
|
|
427
|
+
document.querySelector('.sidebar').classList.toggle('open');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Close nav when clicking outside on mobile
|
|
431
|
+
document.addEventListener('click', (e) => {
|
|
432
|
+
const sidebar = document.querySelector('.sidebar');
|
|
433
|
+
const toggle = document.querySelector('.nav-toggle');
|
|
434
|
+
if (sidebar.classList.contains('open') &&
|
|
435
|
+
!sidebar.contains(e.target) &&
|
|
436
|
+
!toggle.contains(e.target)) {
|
|
437
|
+
sidebar.classList.remove('open');
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
</script>
|
|
441
|
+
|
|
442
|
+
</body>
|
|
443
|
+
</html>
|