create-volt 0.31.0 → 0.33.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 (62) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/addons/posts/files/lib/posts.js +4 -1
  3. package/index.js +1 -1
  4. package/package.json +1 -1
  5. package/templates/blog/Dockerfile +20 -0
  6. package/templates/blog/Procfile +1 -0
  7. package/templates/blog/README.md +38 -0
  8. package/templates/blog/dockerignore +6 -0
  9. package/templates/blog/env +3 -0
  10. package/templates/blog/fly.toml +15 -0
  11. package/templates/blog/gitignore +5 -0
  12. package/templates/blog/package.json +17 -0
  13. package/templates/blog/pages/_theme.js +35 -0
  14. package/templates/blog/pages/about.md +8 -0
  15. package/templates/blog/posts/2026-06-10-themes.md +11 -0
  16. package/templates/blog/posts/2026-06-20-markdown-and-seo.md +14 -0
  17. package/templates/blog/posts/2026-06-28-hello-volt.md +17 -0
  18. package/templates/blog/public/favicon.webp +0 -0
  19. package/templates/blog/public/logo.webp +0 -0
  20. package/templates/blog/public/volt-ssr.js +63 -0
  21. package/templates/blog/public/volt.js +273 -0
  22. package/templates/blog/render.yaml +15 -0
  23. package/templates/blog/server.js +464 -0
  24. package/templates/blog/setup/index.html +28 -0
  25. package/templates/blog/setup/setup.js +200 -0
  26. package/templates/blog/setup/studio.html +29 -0
  27. package/templates/blog/views/index.html +25 -0
  28. package/templates/default/public/favicon.webp +0 -0
  29. package/templates/default/public/logo.webp +0 -0
  30. package/templates/default/server.js +4 -0
  31. package/templates/default/setup/index.html +2 -1
  32. package/templates/default/setup/studio.html +2 -1
  33. package/templates/default/views/index.html +4 -5
  34. package/templates/docs/Dockerfile +20 -0
  35. package/templates/docs/Procfile +1 -0
  36. package/templates/docs/README.md +24 -0
  37. package/templates/docs/dockerignore +6 -0
  38. package/templates/docs/env +2 -0
  39. package/templates/docs/fly.toml +15 -0
  40. package/templates/docs/gitignore +5 -0
  41. package/templates/docs/package.json +17 -0
  42. package/templates/docs/pages/_theme.js +32 -0
  43. package/templates/docs/pages/configuration.md +13 -0
  44. package/templates/docs/pages/deployment.md +9 -0
  45. package/templates/docs/pages/getting-started.md +14 -0
  46. package/templates/docs/public/favicon.webp +0 -0
  47. package/templates/docs/public/logo.webp +0 -0
  48. package/templates/docs/public/volt-ssr.js +63 -0
  49. package/templates/docs/public/volt.js +273 -0
  50. package/templates/docs/render.yaml +15 -0
  51. package/templates/docs/server.js +464 -0
  52. package/templates/docs/setup/index.html +28 -0
  53. package/templates/docs/setup/setup.js +200 -0
  54. package/templates/docs/setup/studio.html +29 -0
  55. package/templates/docs/views/index.html +15 -0
  56. package/templates/starter/public/app.js +1 -1
  57. package/templates/starter/public/favicon.webp +0 -0
  58. package/templates/starter/public/logo.webp +0 -0
  59. package/templates/starter/server.js +4 -0
  60. package/templates/starter/setup/index.html +2 -1
  61. package/templates/starter/setup/studio.html +2 -1
  62. package/templates/starter/views/index.html +2 -0
@@ -0,0 +1,200 @@
1
+ // setup.js — first-run / --edit wizard, built with Volt. Tick add-ons + fill
2
+ // settings → writes .env (a VOLT_ADDONS list + settings), adds any needed
3
+ // packages, installs, and starts the app. Add-on code is bundled; enabling is
4
+ // just config.
5
+ import { signal, computed, html, mount } from "/volt.js";
6
+
7
+ const { available, current, defaultPort } = await (await fetch("/setup/state")).json();
8
+ const depsOf = Object.fromEntries(available.map((a) => [a.name, a.dependsOn || []]));
9
+ const order = available.map((a) => a.name);
10
+ const enabledNow = new Set(String(current.VOLT_ADDONS || "").split(",").map((s) => s.trim()).filter(Boolean));
11
+
12
+ const state = signal({
13
+ addons: Object.fromEntries(available.map((a) => [a.name, enabledNow.has(a.name)])),
14
+ dbDriver: current.DB_DRIVER || "memory",
15
+ mongoUri: current.MONGODB_URI || "",
16
+ mongoDb: current.MONGODB_DATABASE || "",
17
+ dbUrl: current.DATABASE_URL || "",
18
+ smtpUrl: current.SMTP_URL || "",
19
+ mailFrom: current.MAIL_FROM || "",
20
+ mediaDriver: current.MEDIA_DRIVER || "local",
21
+ s3Endpoint: current.S3_ENDPOINT || "",
22
+ s3Region: current.S3_REGION || "",
23
+ s3Bucket: current.S3_BUCKET || "",
24
+ s3Key: current.S3_KEY || "",
25
+ s3Secret: current.S3_SECRET || "",
26
+ s3PublicBase: current.S3_PUBLIC_BASE || "",
27
+ port: current.PORT || String(defaultPort),
28
+ });
29
+ const set = (patch) => state({ ...state(), ...patch });
30
+ const toggle = (n) => state({ ...state(), addons: { ...state().addons, [n]: !state().addons[n] } });
31
+ const status = signal("");
32
+
33
+ // selected add-ons, dependencies expanded, in display order
34
+ function effective(s) {
35
+ const want = new Set();
36
+ const visit = (n) => {
37
+ if (want.has(n)) return;
38
+ want.add(n);
39
+ (depsOf[n] || []).forEach(visit);
40
+ };
41
+ for (const n of order) if (s.addons[n]) visit(n);
42
+ return order.filter((n) => want.has(n));
43
+ }
44
+
45
+ const clean = (v) => String(v).replace(/[\r\n]/g, "").trim(); // one value per line; no injection
46
+ function genEnv(s) {
47
+ const eff = effective(s);
48
+ const out = [`VOLT_ADDONS=${eff.join(",")}`, `PORT=${clean(s.port)}`];
49
+ if (eff.includes("db")) {
50
+ out.push(`DB_DRIVER=${clean(s.dbDriver)}`);
51
+ if (s.dbDriver === "mongodb") {
52
+ out.push(`MONGODB_URI=${clean(s.mongoUri)}`);
53
+ if (s.mongoDb) out.push(`MONGODB_DATABASE=${clean(s.mongoDb)}`);
54
+ } else if (s.dbDriver === "mysql" || s.dbDriver === "postgres") {
55
+ out.push(`DATABASE_URL=${clean(s.dbUrl)}`);
56
+ }
57
+ }
58
+ if (eff.includes("mailer")) {
59
+ if (s.smtpUrl) out.push(`SMTP_URL=${clean(s.smtpUrl)}`);
60
+ else out.push("# SMTP_URL= # unset → emails print to the console");
61
+ if (s.mailFrom) out.push(`MAIL_FROM=${clean(s.mailFrom)}`);
62
+ }
63
+ if (eff.includes("media")) {
64
+ out.push(`MEDIA_DRIVER=${clean(s.mediaDriver)}`);
65
+ if (s.mediaDriver === "s3") {
66
+ out.push(`S3_ENDPOINT=${clean(s.s3Endpoint)}`);
67
+ out.push(`S3_REGION=${clean(s.s3Region)}`);
68
+ out.push(`S3_BUCKET=${clean(s.s3Bucket)}`);
69
+ out.push(`S3_KEY=${clean(s.s3Key)}`);
70
+ out.push(`S3_SECRET=${clean(s.s3Secret)}`);
71
+ if (s.s3PublicBase) out.push(`S3_PUBLIC_BASE=${clean(s.s3PublicBase)}`);
72
+ }
73
+ }
74
+ return out.join("\n") + "\n";
75
+ }
76
+ const env = computed(() => genEnv(state()));
77
+ const eff = computed(() => effective(state()));
78
+ // Memoized, primitive-valued derivations: a conditional section keyed on these
79
+ // only re-renders when the *discriminant* changes — not on every keystroke in a
80
+ // field it contains (which would recreate the input and drop focus).
81
+ const dbDriver = computed(() => state().dbDriver);
82
+ const mediaDriver = computed(() => state().mediaDriver);
83
+ const hasDb = computed(() => eff().includes("db"));
84
+ const hasMailer = computed(() => eff().includes("mailer"));
85
+ const hasMedia = computed(() => eff().includes("media"));
86
+
87
+ async function testDb() {
88
+ const s = state();
89
+ const e = { DB_DRIVER: s.dbDriver };
90
+ if (s.dbDriver === "mongodb") {
91
+ e.MONGODB_URI = s.mongoUri;
92
+ e.MONGODB_DATABASE = s.mongoDb;
93
+ } else if (s.dbDriver === "mysql" || s.dbDriver === "postgres") {
94
+ e.DATABASE_URL = s.dbUrl;
95
+ }
96
+ status("Testing connection…");
97
+ try {
98
+ const r = await (await fetch("/setup/test-db", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ env: e }) })).json();
99
+ status(r.ok ? `✓ Connected (${r.driver}).` : `✗ ${r.error}`);
100
+ } catch {
101
+ status("Network error testing connection.");
102
+ }
103
+ }
104
+
105
+ async function apply() {
106
+ status("Saving…");
107
+ let d;
108
+ try {
109
+ d = await (await fetch("/setup/apply", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ addons: eff(), env: env() }) })).json();
110
+ } catch {
111
+ return status("Network error.");
112
+ }
113
+ if (!d.ok) return status("Error: " + d.error);
114
+ status(d.installing?.length ? `Installing ${d.installing.join(", ")}, then starting…` : "Starting the app…");
115
+ const target = `http://localhost:${d.port}/`;
116
+ const tries = d.installing?.length ? 90 : 20; // npm install can take a while
117
+ const go = async (n) => {
118
+ try {
119
+ await fetch(target, { mode: "no-cors" });
120
+ location.href = target;
121
+ } catch {
122
+ if (n > 0) setTimeout(() => go(n - 1), 500);
123
+ else location.href = target;
124
+ }
125
+ };
126
+ setTimeout(() => go(tries), 600);
127
+ }
128
+
129
+ // --- views ---
130
+ const field = (label, key, placeholder = "") =>
131
+ html`<div class="mb-2">
132
+ <label class="form-label small mb-1">${label}</label>
133
+ <input class="form-control" placeholder=${placeholder} value=${() => state()[key]} oninput=${(e) => set({ [key]: e.target.value })} />
134
+ </div>`;
135
+
136
+ const addonRow = (a) =>
137
+ html`<div class="form-check mb-2">
138
+ <input class="form-check-input" type="checkbox" id=${"x-" + a.name} checked=${() => state().addons[a.name]} onchange=${() => toggle(a.name)} />
139
+ <label class="form-check-label" for=${"x-" + a.name}>
140
+ <span class="accent">${a.name}</span>${a.dependsOn?.length ? html` <span class="text-muted small">(needs ${a.dependsOn.join(", ")})</span>` : ""}
141
+ <div class="small text-muted">${a.description}</div>
142
+ </label>
143
+ </div>`;
144
+
145
+ const dbSettings = () =>
146
+ html`<div class="mb-2">
147
+ <label class="form-label small mb-1">Database (DB_DRIVER)</label>
148
+ <select class="form-select" value=${() => dbDriver()} onchange=${(e) => set({ dbDriver: e.target.value })}>
149
+ <option value="memory">memory (no setup)</option>
150
+ <option value="mongodb">mongodb</option>
151
+ <option value="mysql">mysql</option>
152
+ <option value="postgres">postgres</option>
153
+ </select>
154
+ </div>
155
+ ${() =>
156
+ dbDriver() === "mongodb"
157
+ ? html`${field("MONGODB_URI", "mongoUri", "mongodb://user:pass@host:27017/db")}${field("MONGODB_DATABASE", "mongoDb", "db")}`
158
+ : dbDriver() === "mysql" || dbDriver() === "postgres"
159
+ ? field("DATABASE_URL", "dbUrl", dbDriver() + "://user:pass@host/db")
160
+ : null}
161
+ ${() => (dbDriver() !== "memory" ? html`<button class="btn btn-sm btn-outline-secondary mb-2" onclick=${testDb}>Test connection</button>` : null)}`;
162
+
163
+ const mediaSettings = () =>
164
+ html`<div class="mb-2">
165
+ <label class="form-label small mb-1">Media storage (MEDIA_DRIVER)</label>
166
+ <select class="form-select" value=${() => mediaDriver()} onchange=${(e) => set({ mediaDriver: e.target.value })}>
167
+ <option value="local">local (disk)</option>
168
+ <option value="s3">s3 — AWS S3 / DigitalOcean Spaces</option>
169
+ </select>
170
+ </div>
171
+ ${() =>
172
+ mediaDriver() === "s3"
173
+ ? html`${field("S3_ENDPOINT", "s3Endpoint", "https://nyc3.digitaloceanspaces.com")}${field("S3_REGION", "s3Region", "us-east-1")}${field("S3_BUCKET", "s3Bucket", "my-space")}${field("S3_KEY", "s3Key", "access key")}${field("S3_SECRET", "s3Secret", "secret key")}${field("S3_PUBLIC_BASE (optional CDN base)", "s3PublicBase", "https://cdn.example.com")}`
174
+ : null}`;
175
+
176
+ mount(
177
+ "#app",
178
+ available.length
179
+ ? html`<div class="card-x p-4 mb-3">
180
+ <h2 class="h6 mb-3">Features</h2>
181
+ ${available.map(addonRow)}
182
+ <p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p>
183
+ </div>`
184
+ : null,
185
+ html`<div class="card-x p-4 mb-3">
186
+ <h2 class="h6 mb-3">Settings</h2>
187
+ ${field("PORT", "port", String(defaultPort))}
188
+ ${() => (hasDb() ? dbSettings() : null)}
189
+ ${() => (hasMailer() ? html`${field("SMTP_URL (optional)", "smtpUrl", "smtp://user:pass@smtp.host:587")}${field("MAIL_FROM", "mailFrom", "App <no-reply@you.com>")}` : null)}
190
+ ${() => (hasMedia() ? mediaSettings() : null)}
191
+ </div>`,
192
+ html`<div class="card-x p-4 mb-3">
193
+ <div class="d-flex justify-content-between align-items-center mb-2">
194
+ <h2 class="h6 mb-0">.env</h2>
195
+ <button class="btn btn-primary btn-sm" onclick=${apply}>Apply & start →</button>
196
+ </div>
197
+ <pre class="mb-0" style="background:#0b0d11;border:1px solid #232a36;border-radius:10px;padding:12px;color:#cfe3ff;white-space:pre-wrap">${env}</pre>
198
+ </div>`,
199
+ () => (status() ? html`<p class="small accent">${status}</p>` : null),
200
+ );
@@ -0,0 +1,29 @@
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" />
6
+ <link rel="icon" href="/favicon.webp" />
7
+ <title>Volt Studio</title>
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
9
+ <style>
10
+ body { background: #0f1115; color: #e7e9ee; }
11
+ .accent { color: #ffd24a; }
12
+ .card-x { background: #161a22; border: 1px solid #232a36; border-radius: 14px; }
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <main class="container py-5" style="max-width: 820px;">
17
+ <header class="mb-4">
18
+ <h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Volt Studio</span> <small class="text-muted">data browser</small></h1>
19
+ <p class="text-muted mb-0">Browse the database in your <code>.env</code>. Disposable &amp; localhost-only — close the terminal when done.</p>
20
+ </header>
21
+ <div id="app"></div>
22
+ </main>
23
+ <script type="module">
24
+ import { mount } from "/volt.js";
25
+ import { dbAdminPanel } from "/db-admin-ui.js";
26
+ mount("#app", dbAdminPanel());
27
+ </script>
28
+ </body>
29
+ </html>
@@ -0,0 +1,25 @@
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" />
6
+ <title>My Volt Blog</title>
7
+ <meta name="description" content="A blog built with Volt — markdown posts, a theme, and real SEO." />
8
+ <link rel="icon" href="/favicon.webp" />
9
+ <!-- the same stylesheet the posts/pages use, so the home matches the theme -->
10
+ <link rel="stylesheet" href="/_theme.css" />
11
+ </head>
12
+ <body>
13
+ <header class="site"><div class="wrap"><a class="brand" href="/"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> My Volt Blog</a>
14
+ <nav><a href="/blog">Blog</a><a href="/about">About</a><a href="/feed.xml">RSS</a></nav></div></header>
15
+ <main><div class="wrap">
16
+ <h1>A blog built with Volt</h1>
17
+ <p>Markdown posts in <code>posts/</code>, pages in <code>pages/</code>, one theme in
18
+ <code>pages/_theme.js</code>. No build step, no database, no admin to attack.</p>
19
+ <p><a href="/blog">Read the blog →</a></p>
20
+ <p style="opacity:.7;font-size:.95rem">Edit any <code>.md</code> file and refresh. Add posts, change the theme,
21
+ or run <code>npm run dev -- --edit</code> to add features (auth, a database, the WYSIWYG editor).</p>
22
+ </div></main>
23
+ <footer class="site"><div class="wrap">My Volt Blog — built with Volt</div></footer>
24
+ </body>
25
+ </html>
@@ -225,6 +225,8 @@ function startSetup() {
225
225
  const assets = {
226
226
  "/setup.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "setup", "setup.js"))],
227
227
  "/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
228
+ "/logo.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "logo.webp"))],
229
+ "/favicon.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "favicon.webp"))],
228
230
  };
229
231
  const indexHtml = fs.readFileSync(path.join(__dirname, "setup", "index.html"));
230
232
 
@@ -381,6 +383,8 @@ async function startStudio() {
381
383
  const visible = (n) => n && !HIDDEN_COLLECTIONS.has(n);
382
384
  const assets = {
383
385
  "/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
386
+ "/logo.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "logo.webp"))],
387
+ "/favicon.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "favicon.webp"))],
384
388
  "/db-admin-ui.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(ADDONS_DIR, "db", "files", "public", "db-admin-ui.js"))],
385
389
  };
386
390
  const studioHtml = fs.readFileSync(path.join(__dirname, "setup", "studio.html"));
@@ -3,6 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <link rel="icon" href="/favicon.webp" />
6
7
  <title>Set up your Volt app</title>
7
8
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
8
9
  <style>
@@ -17,7 +18,7 @@
17
18
  <body>
18
19
  <main class="container py-5" style="max-width: 640px;">
19
20
  <header class="mb-4">
20
- <h1 class="h3"><span class="accent">⚡ Set up your Volt app</span></h1>
21
+ <h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Set up your Volt app</span></h1>
21
22
  <p class="text-muted mb-0">Fill these in and the app starts. This page is disposable — it disappears once you click Apply. Re-open it anytime with <code>npm run dev -- --edit</code>.</p>
22
23
  </header>
23
24
  <div id="app"></div>
@@ -3,6 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <link rel="icon" href="/favicon.webp" />
6
7
  <title>Volt Studio</title>
7
8
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
8
9
  <style>
@@ -14,7 +15,7 @@
14
15
  <body>
15
16
  <main class="container py-5" style="max-width: 820px;">
16
17
  <header class="mb-4">
17
- <h1 class="h3"><span class="accent">⚡ Volt Studio</span> <small class="text-muted">data browser</small></h1>
18
+ <h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Volt Studio</span> <small class="text-muted">data browser</small></h1>
18
19
  <p class="text-muted mb-0">Browse the database in your <code>.env</code>. Disposable &amp; localhost-only — close the terminal when done.</p>
19
20
  </header>
20
21
  <div id="app"></div>
@@ -3,6 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <link rel="icon" href="/favicon.webp" />
6
7
  <title>Volt — signals, no build, hot reload</title>
7
8
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
8
9
  <style>
@@ -14,12 +15,13 @@
14
15
  .form-control:focus { background: #0f1115; color: #e7e9ee; border-color: #ffd24a; box-shadow: none; }
15
16
  .list-group-item { background: #0f1115; color: #e7e9ee; border-color: #232a36; }
16
17
  a { color: #ffd24a; }
18
+ .text-muted { color: #9aa4b2 !important; } /* readable muted on the dark bg */
17
19
  </style>
18
20
  </head>
19
21
  <body>
20
22
  <main class="container py-5" style="max-width: 720px;">
21
23
  <header class="text-center mb-5">
22
- <h1 class="brand display-5"><span class="accent">⚡ Volt</span></h1>
24
+ <h1 class="brand display-5"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Volt</span></h1>
23
25
  <p class="text-muted mb-0">
24
26
  Fine-grained signals · no JSX · no virtual DOM · live hot reload.
25
27
  Edit <code>public/app.js</code> and save — this page reloads itself.
@@ -29,10 +31,7 @@
29
31
  <div id="app"></div>
30
32
 
31
33
  <footer class="text-center text-muted mt-5">
32
- <small>Both cards run on the same signal engine one uses <code>el()</code>, the other <code>html``</code>.</small>
33
- <div class="mt-2">
34
- <small><a href="https://github.com/MIR-2025/volt#readme" target="_blank" rel="noopener">📖 How to build a Volt app →</a></small>
35
- </div>
34
+ <small><a href="https://github.com/MIR-2025/volt#readme" target="_blank" rel="noopener">📖 How to build a Volt app →</a></small>
36
35
  </footer>
37
36
  </main>
38
37
 
@@ -0,0 +1,20 @@
1
+ # Volt app — production container. Runs on Render, Fly.io, Railway, DO App
2
+ # Platform, and any container host. They handle the server, DNS, and TLS; you
3
+ # just set config as env vars.
4
+ #
5
+ # Configure via the platform's env vars (NOT a committed .env):
6
+ # VOLT_ADDONS=db,auth,... DB_DRIVER=... MONGODB_URI / DATABASE_URL
7
+ # MEDIA_DRIVER=s3 S3_ENDPOINT/S3_REGION/S3_BUCKET/S3_KEY/S3_SECRET etc.
8
+ #
9
+ # Tip: run the local wizard first (`npm run dev`) so the add-on packages are
10
+ # saved into package.json; commit package.json, then deploy and set the same
11
+ # config as env vars here. NODE_ENV=production makes the app boot straight up
12
+ # (no setup wizard).
13
+ FROM node:22-alpine
14
+ WORKDIR /app
15
+ ENV NODE_ENV=production
16
+ COPY package*.json ./
17
+ RUN npm install --omit=dev
18
+ COPY . .
19
+ EXPOSE 8080
20
+ CMD ["node", "server.js"]
@@ -0,0 +1 @@
1
+ web: node server.js
@@ -0,0 +1,24 @@
1
+ # Volt docs
2
+
3
+ A documentation site built with [Volt](https://voltjs.com) — markdown pages in a sidebar layout. No build step, no database.
4
+
5
+ ```
6
+ npm install
7
+ npm run dev # → http://localhost:26629 (redirects to /getting-started)
8
+ ```
9
+
10
+ ## Where things live
11
+
12
+ | Path | What |
13
+ |---|---|
14
+ | `pages/*.md` | Each becomes a doc page at `/<slug>` |
15
+ | `pages/_theme.js` | The sidebar layout + CSS. Add pages to the `NAV` list here |
16
+ | `views/index.html` | Redirects `/` to the first page |
17
+ | `.env` | `VOLT_ADDONS=pages`, `SITE_NAME` |
18
+
19
+ ## Add a page
20
+
21
+ 1. Create `pages/my-topic.md` with front-matter (`title`, `description`).
22
+ 2. Add `["/my-topic", "My topic"]` to `NAV` in `pages/_theme.js`.
23
+
24
+ Run `npm run dev -- --edit` to add features (auth, a database, the WYSIWYG editor), or set `THEME=<name>` to use a published `volt-theme-*`.
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ .git
3
+ .env
4
+ *.log
5
+ npm-debug.log*
6
+ .DS_Store
@@ -0,0 +1,2 @@
1
+ VOLT_ADDONS=pages
2
+ SITE_NAME=Docs
@@ -0,0 +1,15 @@
1
+ # Fly.io — run `fly launch` (it uses the Dockerfile), then set config:
2
+ # fly secrets set VOLT_ADDONS=db,auth DB_DRIVER=mongodb MONGODB_URI=...
3
+ app = "volt-app"
4
+
5
+ [build]
6
+
7
+ [http_service]
8
+ internal_port = 8080
9
+ force_https = true
10
+ auto_stop_machines = true
11
+ auto_start_machines = true
12
+
13
+ [env]
14
+ PORT = "8080"
15
+ NODE_ENV = "production"
@@ -0,0 +1,5 @@
1
+ node_modules
2
+ npm-debug.log*
3
+ .DS_Store
4
+ .env
5
+ *.local
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "volt-docs",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "A Volt app — no-build, signals-based UI with Socket.io hot reload.",
6
+ "type": "module",
7
+ "main": "server.js",
8
+ "scripts": {
9
+ "start": "node server.js",
10
+ "dev": "node server.js"
11
+ },
12
+ "dependencies": {
13
+ "express": "^4.22.2",
14
+ "socket.io": "^4.8.3",
15
+ "marked": "^18.0.5"
16
+ }
17
+ }
@@ -0,0 +1,32 @@
1
+ // _theme.js — the docs theme: a fixed sidebar + a content column. Add a page in
2
+ // pages/ and link it in NAV below. `css` is served at /_theme.css.
3
+ const NAME = process.env.SITE_NAME || "Docs";
4
+ const NAV = [
5
+ ["/getting-started", "Getting started"],
6
+ ["/configuration", "Configuration"],
7
+ ["/deployment", "Deployment"],
8
+ ];
9
+
10
+ export const css = `:root{--ink:#1f2329;--bg:#fff;--accent:#2557d6;--muted:#5b6573;--line:#e7eaef;--side:#f7f8fa}
11
+ *{box-sizing:border-box}
12
+ body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.7 system-ui,-apple-system,sans-serif;display:flex;min-height:100vh}
13
+ aside{width:240px;flex:0 0 240px;background:var(--side);border-right:1px solid var(--line);padding:1.5rem}
14
+ aside .brand{display:flex;align-items:center;gap:.4rem;font-weight:800;color:var(--ink);text-decoration:none;margin-bottom:1rem}
15
+ aside nav a{display:block;color:var(--muted);text-decoration:none;padding:.3rem 0}
16
+ aside nav a:hover{color:var(--accent)}
17
+ main{flex:1;min-width:0;padding:2.5rem;max-width:780px}
18
+ h1,h2,h3{line-height:1.25}
19
+ a{color:var(--accent)}
20
+ pre{background:#0b0d11;color:#cfe3ff;padding:1rem;border-radius:8px;overflow:auto}
21
+ :not(pre)>code{background:#eef1f5;padding:.1em .35em;border-radius:5px}
22
+ img{max-width:100%}
23
+ @media(max-width:700px){body{flex-direction:column}aside{width:auto;flex:none;border-right:none;border-bottom:1px solid var(--line)}}`;
24
+
25
+ export function layout({ title, head, content }) {
26
+ const nav = NAV.map(([href, label]) => `<a href="${href}">${label}</a>`).join("");
27
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
28
+ <title>${title}</title>${head}<link rel="icon" href="/favicon.webp" /><link rel="stylesheet" href="/_theme.css" /></head><body>
29
+ <aside><a class="brand" href="/getting-started"><img src="/logo.webp" alt="" style="height:1.1em" /> ${NAME}</a><nav>${nav}</nav></aside>
30
+ <main>${content}</main>
31
+ </body></html>`;
32
+ }
@@ -0,0 +1,13 @@
1
+ ---
2
+ title: Configuration
3
+ description: Everything is environment variables.
4
+ ---
5
+ # Configuration
6
+
7
+ Volt is configured entirely with environment variables (no config-in-database):
8
+
9
+ - `SITE_NAME` — shown in the sidebar.
10
+ - `VOLT_ADDONS` — which features are on (here: `pages`).
11
+ - `THEME` — a published `volt-theme-*`, or use the local `pages/_theme.js`.
12
+
13
+ In dev these live in `.env`; in production set them as platform env vars.
@@ -0,0 +1,9 @@
1
+ ---
2
+ title: Deployment
3
+ description: Ship it anywhere Node runs.
4
+ ---
5
+ # Deployment
6
+
7
+ This template ships a `Dockerfile`, `render.yaml`, `fly.toml`, and a `Procfile`.
8
+ Set `NODE_ENV=production` (and your env vars) and it boots straight into the app
9
+ — no wizard, no `.env` needed.
@@ -0,0 +1,14 @@
1
+ ---
2
+ title: Getting started
3
+ description: Your first five minutes with this docs site.
4
+ ---
5
+ # Getting started
6
+
7
+ Every page here is a markdown file in `pages/`, served at `/<slug>` inside the
8
+ docs theme (`pages/_theme.js`). To add a page, drop a `.md` file and link it in
9
+ the sidebar (`NAV` in the theme).
10
+
11
+ ```
12
+ npm run dev # start
13
+ npm run dev -- --edit # wizard: add auth, a database, the editor…
14
+ ```
Binary file
@@ -0,0 +1,63 @@
1
+ // volt-ssr.js — server-side rendering for Volt. Renders the same html`` markup,
2
+ // h() elements, and signal values to an HTML string in Node (no DOM), so a Volt
3
+ // app can be fully server-rendered for SEO and hydrate interactive islands with
4
+ // volt.js on the client.
5
+ //
6
+ // import { html, h, raw, renderToString } from "./volt-ssr.js";
7
+ // renderToString(html`<p>${name}</p>`) // → "<p>Ada</p>" (name is escaped)
8
+ //
9
+ // Authoring matches the client: html`` interpolations render as escaped text;
10
+ // nest html``/h() nodes for structure; use raw() for trusted pre-built HTML.
11
+
12
+ const VOID = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
13
+ const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
14
+
15
+ // trusted, pre-rendered HTML — emitted verbatim. Use only for content you control.
16
+ export const raw = (s) => ({ __raw: String(s) });
17
+
18
+ // tagged-template markup; the literal chunks are trusted, ${values} are escaped.
19
+ export const html = (strings, ...values) => ({ __tpl: true, strings, values });
20
+
21
+ const isNode = (x) => x && typeof x === "object" && (x.__tpl || x.__raw || x.__el);
22
+
23
+ // hyperscript element: h(tag, props?, ...children)
24
+ export function h(tag, props, ...children) {
25
+ if (props === undefined || props === null || isNode(props) || Array.isArray(props) || typeof props !== "object") {
26
+ if (props !== undefined && props !== null) children.unshift(props);
27
+ props = {};
28
+ }
29
+ return { __el: true, tag, props, children };
30
+ }
31
+
32
+ const read = (v) => (typeof v === "function" ? v() : v); // resolve signals/thunks (once)
33
+
34
+ function attrs(props) {
35
+ let out = "";
36
+ for (const [k, rawVal] of Object.entries(props)) {
37
+ if (k === "children" || k.startsWith("on")) continue; // event handlers don't SSR
38
+ const v = read(rawVal);
39
+ if (v == null || v === false) continue;
40
+ const name = k === "className" ? "class" : k;
41
+ out += v === true ? ` ${name}` : ` ${name}="${esc(v)}"`;
42
+ }
43
+ return out;
44
+ }
45
+
46
+ export function renderToString(node) {
47
+ const v = read(node);
48
+ if (v == null || v === false || v === true) return "";
49
+ if (typeof v === "string" || typeof v === "number") return esc(v);
50
+ if (v.__raw != null) return v.__raw;
51
+ if (Array.isArray(v)) return v.map(renderToString).join("");
52
+ if (v.__tpl) {
53
+ let out = v.strings[0];
54
+ for (let i = 0; i < v.values.length; i++) out += renderToString(v.values[i]) + v.strings[i + 1];
55
+ return out;
56
+ }
57
+ if (v.__el) {
58
+ if (typeof v.tag === "function") return renderToString(v.tag({ ...v.props, children: v.children }));
59
+ const open = `<${v.tag}${attrs(v.props)}>`;
60
+ return VOID.has(v.tag) ? open : `${open}${v.children.map(renderToString).join("")}</${v.tag}>`;
61
+ }
62
+ return esc(String(v));
63
+ }