create-spark-html-app 0.12.0 → 0.13.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.
package/bin/index.js CHANGED
@@ -27,8 +27,6 @@ import { createInterface } from 'node:readline/promises';
27
27
  import { stdin, stdout, argv, exit } from 'node:process';
28
28
 
29
29
  const here = dirname(fileURLToPath(import.meta.url));
30
- const templateFor = (type) =>
31
- resolve(here, '..', type === 'client' ? 'template' : `template-${type}`);
32
30
 
33
31
  // ── tiny ANSI palette (no chalk; one less thing to install) ───────────
34
32
  const supportsColor = stdout.isTTY && process.env.NO_COLOR === undefined;
@@ -122,6 +120,24 @@ async function pickType() {
122
120
  return (TYPES[Number(a) - 1] || TYPES[0]).key;
123
121
  }
124
122
 
123
+ // SSR only: with a database (SQLite + auto-CRUD + auth + admin) or without one
124
+ // (a markdown blog on file sources). Flags: --db / --no-db.
125
+ async function pickSsrDb() {
126
+ const flags = argv.slice(2);
127
+ if (flags.includes('--no-db')) return false;
128
+ if (flags.includes('--db')) return true;
129
+ if (!stdin.isTTY) return true;
130
+ const a = (await prompt(`${c.accent('?')} Include a database? ${c.dim('(Y/n)')} `, 'y')).toLowerCase();
131
+ return /^y(es)?$/.test(a);
132
+ }
133
+
134
+ // Resolve the template directory for a chosen project type.
135
+ function templateDir(type, ssrDb) {
136
+ if (type === 'client') return resolve(here, '..', 'template');
137
+ if (type === 'prerender') return resolve(here, '..', 'template-prerender');
138
+ return resolve(here, '..', ssrDb ? 'template-ssr' : 'template-ssr-nodb');
139
+ }
140
+
125
141
  // ── optional features ──────────────────────────────────────────────────
126
142
  // The template ships with EVERYTHING wired; excluded features are stripped
127
143
  // out of the copied files via `@spark:<name>` … `@spark:end` marker blocks
@@ -232,11 +248,13 @@ async function main() {
232
248
 
233
249
  // 3 ─ pick the project type + features, copy the template ────────────
234
250
  const type = await pickType();
251
+ // SSR can ship with or without a database; the choice picks the template.
252
+ const ssrDb = type === 'ssr' ? await pickSsrDb() : true;
235
253
  // The prerender template is the full showcase (router, todos, demos) whose
236
254
  // optional pieces are toggled via @spark markers; client and ssr ship fixed.
237
255
  const features = type === 'prerender' ? await pickFeatures() : {};
238
256
  mkdirSync(targetDir, { recursive: true });
239
- cpSync(templateFor(type), targetDir, { recursive: true });
257
+ cpSync(templateDir(type, ssrDb), targetDir, { recursive: true });
240
258
  if (type === 'prerender') applyFeatures(targetDir, features);
241
259
 
242
260
  // npm renames/strips dotfiles on publish, so the template ships them
@@ -287,7 +305,8 @@ async function main() {
287
305
 
288
306
  // 5 ─ celebrate + print next steps ───────────────────────────────────
289
307
  const rel = relative(process.cwd(), targetDir) || '.';
290
- const flavor = type === 'ssr' ? 'SSR — spark-ssr, zero config, no build'
308
+ const flavor = type === 'ssr'
309
+ ? (ssrDb ? 'SSR — spark-ssr blog, SQLite + auth, zero config' : 'SSR — spark-ssr, markdown blog, no database')
291
310
  : type === 'prerender' ? `prerendered showcase — router, todos, demos + ${FEATURES.filter((f) => features[f.key]).map((f) => f.key).join(', ') || 'core only'}`
292
311
  : 'client-only — a single reactive counter to build on';
293
312
  stdout.write(`\n${c.green('✔')} Scaffolded ${c.bold(projectName)} in ${c.cyan(rel)} ${c.dim(`(${flavor})`)}\n\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-spark-html-app",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Scaffold a spark-html app — dev/build/preview on Bun",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "bin",
11
11
  "template",
12
12
  "template-ssr",
13
+ "template-ssr-nodb",
13
14
  "template-prerender"
14
15
  ],
15
16
  "engines": {
@@ -0,0 +1,32 @@
1
+ # ⚡ Spark Notes — a database-free spark-ssr blog
2
+
3
+ A server-rendered blog with **no database**. Every post is a markdown file in
4
+ `content/`, read through spark-ssr's file **sources**:
5
+
6
+ - `posts = ./content/*.md` — a **glob source** (front matter → columns,
7
+ filename → slug) lists posts on the homepage.
8
+ - `post = ./lib/post.js` — a **module source** reads one post by `:slug` for
9
+ each `/blog/:slug` page.
10
+
11
+ No table, no query, no ORM, no build step.
12
+
13
+ ```bash
14
+ bun install
15
+ bun run dev # dev server with live reload
16
+ bun run build # dist/ (a single binary) for production
17
+ bun run start # run the production server
18
+ ```
19
+
20
+ ## Add a post
21
+
22
+ Drop a markdown file in `content/` with `title`, `date`, and `excerpt` front
23
+ matter. The filename is its URL (`content/my-post.md` → `/blog/my-post`).
24
+
25
+ ## Need a database?
26
+
27
+ Users, comments, orders, admin CRUD, auth, `live` updates — scaffold the SSR
28
+ template **with a database** instead:
29
+
30
+ ```bash
31
+ bun create spark-html-app my-app # choose SSR, then "with database"
32
+ ```
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ dist
@@ -0,0 +1,11 @@
1
+ <!-- Pure UI — no <script>. `blog` is a prop from the layout; `path` is ambient
2
+ page scope (the current request path), so the highlight is right on the
3
+ server render and after client mount alike. -->
4
+ <nav class="nav">
5
+ <a class="brand" href="/">⚡ {blog}</a>
6
+ <span class="links">
7
+ <a href="/" :class="path === '/' ? 'on' : ''">Posts</a>
8
+ <a href="/about" :class="path === '/about' ? 'on' : ''">About</a>
9
+ <span import="/components/theme-toggle"></span>
10
+ </span>
11
+ </nav>
@@ -0,0 +1,8 @@
1
+ <!-- Pure UI — receives a whole post row (from the glob source) as one prop:
2
+ <div import="/components/post-card" post="{post}">. A markdown file's
3
+ front matter became these columns; the filename became {post.slug}. -->
4
+ <article class="card">
5
+ <h2><a href="/blog/{post.slug}">{post.title}</a></h2>
6
+ <p>{post.excerpt}</p>
7
+ <p class="meta">{post.date}</p>
8
+ </article>
@@ -0,0 +1,9 @@
1
+ <!-- The spark-html-theme store, as a button. theme() itself boots in
2
+ /app.js; the server also inlines the no-flash init snippet in <head>. -->
3
+ <button class="ghost toggle" onclick={theme.toggle} aria-label="Toggle theme">
4
+ {theme.resolved === 'dark' ? '☀️' : '🌙'}
5
+ </button>
6
+
7
+ <script>
8
+ const theme = useStore('theme');
9
+ </script>
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: Hello, Spark
3
+ date: 2026-07-05
4
+ excerpt: A blog that runs on markdown files — no database, no build step.
5
+ ---
6
+ This post is a markdown file at content/hello-spark.md. Its front matter became
7
+ the title, date, and excerpt you see; the text below it is the body.
8
+
9
+ spark-ssr read the file, rendered this page on the server, and hydrated it in
10
+ the browser — all from the template. Edit this file and save to see it change.
11
+
12
+ To publish a new post, drop another .md file in content/. That's the whole CMS.
@@ -0,0 +1,14 @@
1
+ ---
2
+ title: Why no database?
3
+ date: 2026-07-03
4
+ excerpt: Content sites don't need a database. Files are simpler, versioned, and fast.
5
+ ---
6
+ A blog, docs site, changelog, or portfolio is just content. Putting it in a
7
+ database means migrations, a running server process, and backups — for text you
8
+ already have in files.
9
+
10
+ spark-ssr's glob source turns a folder of markdown into rows. Your posts live in
11
+ git, review in pull requests, and deploy as static files that render on the edge.
12
+
13
+ When you do need a database — users, comments, orders — spark-ssr has that too:
14
+ scaffold the SSR template with a database and declare a table in your template.
@@ -0,0 +1,14 @@
1
+ ---
2
+ title: The template is the site
3
+ date: 2026-06-28
4
+ excerpt: Filesystem routing, layouts, and sources — the HTML says what it needs.
5
+ ---
6
+ pages/index.html is the homepage. pages/blog/[slug].html is every post. pages/
7
+ _layout.html wraps them with the nav and footer, once. The filesystem is the
8
+ router; no config maps URLs to files.
9
+
10
+ The <spark-ssr> block names the data and where it comes from. Here it is a glob
11
+ of markdown files and a small module — but it could be a SQL table, a URL, or a
12
+ JavaScript function. Same block, different worlds.
13
+
14
+ No <script> tags for the basics, no server file, no ORM. The template is enough.
@@ -0,0 +1,23 @@
1
+ // A MODULE source: `post = ./lib/post.js` in blog/[slug].html calls this with
2
+ // the request. It reads a single markdown file by slug and returns one row
3
+ // (front matter → fields, the rest → body), or null when there's no such file
4
+ // — which the page turns into a real 404. No database required.
5
+ import { readFileSync, existsSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+
8
+ export default function post(req) {
9
+ const slug = String(req.params.slug || '').replace(/[^a-z0-9-]/gi, '');
10
+ const file = join(import.meta.dir, '..', 'content', `${slug}.md`);
11
+ if (!slug || !existsSync(file)) return null;
12
+
13
+ const raw = readFileSync(file, 'utf8');
14
+ const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
15
+ const data = {};
16
+ if (m) {
17
+ for (const line of m[1].split('\n')) {
18
+ const kv = line.match(/^([\w-]+)\s*:\s*(.*)$/);
19
+ if (kv) data[kv[1]] = kv[2].trim().replace(/^["'](.*)["']$/, '$1');
20
+ }
21
+ }
22
+ return { slug, ...data, body: m ? raw.slice(m[0].length).trim() : raw.trim() };
23
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "spark-notes",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun spark-ssr",
8
+ "start": "bun spark-ssr start",
9
+ "build": "bun spark-ssr build"
10
+ },
11
+ "dependencies": {
12
+ "spark-html": "latest",
13
+ "spark-ssr": "latest",
14
+ "spark-html-theme": "latest",
15
+ "spark-html-font": "latest"
16
+ }
17
+ }
@@ -0,0 +1,14 @@
1
+ <!-- The layout wraps every page. No database here — this whole site runs on
2
+ markdown files (see content/) read through spark-ssr's glob and module
3
+ sources. The three shared lines live once; <slot> is the page. -->
4
+ <link rel="stylesheet" href="/style.css">
5
+ <script type="module" src="/app.js"></script>
6
+
7
+ <div import="/components/nav" blog="Spark Notes" path="{path}"></div>
8
+
9
+ <slot></slot>
10
+
11
+ <footer class="foot">
12
+ Built with <a href="https://github.com/wilkinnovo/spark-html">spark-ssr</a> —
13
+ no database, just markdown.
14
+ </footer>
@@ -0,0 +1,18 @@
1
+ <!-- A plain static page — no data block at all. spark-ssr still wraps it in the
2
+ layout and ships fully-rendered HTML. -->
3
+ <title>About · Spark Notes</title>
4
+ <meta name="description" content="What this database-free spark-ssr starter is.">
5
+
6
+ <article class="post">
7
+ <h1>About</h1>
8
+ <p class="lede">This is a database-free blog built with <code>spark-ssr</code>.</p>
9
+ <p>Every post is a markdown file in <code>content/</code>. The homepage reads
10
+ them with a <strong>glob source</strong> (<code>posts = ./content/*.md</code>);
11
+ each post page reads one with a <strong>module source</strong>
12
+ (<code>post = ./lib/post.js</code>). There is no database, no table, no query,
13
+ and no build step — just files and HTML.</p>
14
+ <p>Add a post by dropping a new <code>.md</code> file in <code>content/</code>
15
+ with <code>title</code>, <code>date</code>, and <code>excerpt</code> front
16
+ matter. Need a database later? Scaffold the SSR template with a database.</p>
17
+ <p><a href="/">← All posts</a></p>
18
+ </article>
@@ -0,0 +1,27 @@
1
+ <!-- /blog/:slug — the filename IS the route; :slug binds into the source.
2
+ `post = ./lib/post.js` is a MODULE source: it returns one row for the slug
3
+ or null. A missing post is a real 404 (status on the else branch), so
4
+ crawlers and the browser both get the right answer. -->
5
+ <title>{post ? post.title : 'Not found'} · Spark Notes</title>
6
+ <meta name="description" content="{post ? post.excerpt : 'This post does not exist.'}">
7
+
8
+ <template if="post">
9
+ <article class="post">
10
+ <h1>{post.title}</h1>
11
+ <p class="meta">{post.date}</p>
12
+ <p class="lede">{post.excerpt}</p>
13
+ <div class="body">{post.body}</div>
14
+ <p><a href="/">← All posts</a></p>
15
+ </article>
16
+ </template>
17
+ <template else status="404">
18
+ <article class="post">
19
+ <h1>Not found</h1>
20
+ <p class="lede">That post doesn't exist.</p>
21
+ <p><a href="/">← All posts</a></p>
22
+ </article>
23
+ </template>
24
+
25
+ <spark-ssr>
26
+ post = ./lib/post.js
27
+ </spark-ssr>
@@ -0,0 +1,27 @@
1
+ <!-- The homepage. `posts = ./content/*.md` is a GLOB source: every markdown
2
+ file under content/ becomes a row — front matter → columns, filename →
3
+ slug, body → .body. No database, no table, no query. -->
4
+ <title>Spark Notes — a markdown blog</title>
5
+ <meta name="description" content="A database-free blog powered by spark-ssr and markdown files.">
6
+
7
+ <header class="hero">
8
+ <img class="avatar" src="/img/avatar.png" alt="Spark Notes" width="88" height="88">
9
+ <div>
10
+ <h1>Spark Notes</h1>
11
+ <p class="lede">A blog with no database — every post is a markdown file in
12
+ <code>content/</code>. spark-ssr reads them, renders them, and hydrates.</p>
13
+ </div>
14
+ </header>
15
+
16
+ <main>
17
+ <template each="post in posts">
18
+ <div import="/components/post-card" post="{post}"></div>
19
+ </template>
20
+ <template if="posts.length === 0">
21
+ <p class="empty">No posts yet — drop a <code>.md</code> file in <code>content/</code>.</p>
22
+ </template>
23
+ </main>
24
+
25
+ <spark-ssr>
26
+ posts = ./content/*.md
27
+ </spark-ssr>
@@ -0,0 +1,8 @@
1
+ // The client side of the blog — plain browser modules, no build step.
2
+ // spark-ssr serves each family package at /@modules/* and puts them in the
3
+ // page importmap, so bare imports just work.
4
+ import { theme } from 'spark-html-theme';
5
+
6
+ // Dark/light/system with persistence; the server already inlined the
7
+ // no-flash snippet in <head>, this wires the reactive store + toggle.
8
+ theme();
@@ -0,0 +1,175 @@
1
+ /* One stylesheet for the whole blog — the same design system as the Spark
2
+ showcase: monospace JetBrains Mono type, a gold --spark accent, rounded
3
+ surfaces. Fonts come from spark.json ("fonts" → spark-html-font injects
4
+ @font-face + the --font-jetbrains-mono stack); theming is the data-theme
5
+ attribute spark-html-theme maintains (dark is the default). */
6
+
7
+ :root {
8
+ --bg: #000;
9
+ --surface: #0a0a0a;
10
+ --surface-2: #101014;
11
+ --border: #1a1a1a;
12
+ --border-strong: #333;
13
+ --text: #fff;
14
+ --muted: #888;
15
+ --muted-dim: #555;
16
+ --spark: #ffd24a;
17
+ --spark-ink: #ffd24a;
18
+ --danger: #ff6b6b;
19
+ --radius: 12px;
20
+ --font: var(--font-jetbrains-mono, "JetBrains Mono", ui-monospace, monospace);
21
+ }
22
+
23
+ [data-theme="light"] {
24
+ --bg: #fff;
25
+ --surface: #fafafa;
26
+ --surface-2: #f4f4f5;
27
+ --border: #ededed;
28
+ --border-strong: #d4d4d4;
29
+ --text: #1a1a1a;
30
+ --muted: #666;
31
+ --muted-dim: #999;
32
+ --spark: #ffd24a;
33
+ --spark-ink: #9a6a00;
34
+ --danger: #d63b3b;
35
+ }
36
+
37
+ *, *::before, *::after { box-sizing: border-box; }
38
+
39
+ body {
40
+ margin: 0 auto;
41
+ max-width: 720px;
42
+ padding: 0 1.25rem 4rem;
43
+ font-family: var(--font);
44
+ line-height: 1.6;
45
+ background: var(--bg);
46
+ color: var(--text);
47
+ -webkit-font-smoothing: antialiased;
48
+ }
49
+
50
+ a { color: var(--spark-ink); text-decoration: none; }
51
+ a:hover { text-decoration: underline; }
52
+ h1, h2, h3 { line-height: 1.2; letter-spacing: -0.02em; }
53
+ ::selection { background: var(--spark); color: #000; }
54
+ [data-theme="light"] ::selection { background: #ffe9a8; color: #111; }
55
+ code {
56
+ background: var(--surface-2);
57
+ color: var(--spark-ink);
58
+ padding: 1px 6px;
59
+ border-radius: 4px;
60
+ font-size: 0.85em;
61
+ }
62
+
63
+ /* ── nav ── */
64
+ .nav {
65
+ display: flex;
66
+ justify-content: space-between;
67
+ align-items: center;
68
+ gap: 1rem;
69
+ padding: 1rem 0;
70
+ margin-bottom: 1rem;
71
+ border-bottom: 1px solid var(--border);
72
+ }
73
+ .nav .brand {
74
+ font-weight: 700;
75
+ color: var(--text);
76
+ display: inline-flex;
77
+ align-items: center;
78
+ gap: 0.4rem;
79
+ }
80
+ .nav .links { display: flex; align-items: center; gap: 0.35rem; }
81
+ .nav .links a {
82
+ color: var(--muted);
83
+ padding: 0.35rem 0.6rem;
84
+ border-radius: 8px;
85
+ font-size: 0.9rem;
86
+ transition: color 0.12s, background 0.12s;
87
+ }
88
+ .nav .links a:hover { color: var(--text); background: var(--surface-2); text-decoration: none; }
89
+ .nav .links a.on { color: var(--spark-ink); background: var(--surface-2); font-weight: 700; }
90
+
91
+ /* ── home ── */
92
+ .hero { display: flex; gap: 1.25rem; align-items: center; margin: 2rem 0 2.5rem; }
93
+ .hero h1 { margin: 0 0 0.25rem; font-size: clamp(1.6rem, 5vw, 2.2rem); }
94
+ .avatar { border-radius: 50%; border: 1px solid var(--border-strong); }
95
+ .lede { color: var(--muted); margin: 0; }
96
+
97
+ .card {
98
+ background: var(--surface);
99
+ border: 1px solid var(--border);
100
+ border-radius: var(--radius);
101
+ padding: 1rem 1.25rem;
102
+ margin: 0 0 1rem;
103
+ transition: border-color 0.12s;
104
+ }
105
+ .card:hover { border-color: var(--border-strong); }
106
+ .card h2 { margin: 0 0 0.35rem; font-size: 1.1rem; }
107
+ .card h2 a { color: var(--text); }
108
+ .card h2 a:hover { color: var(--spark-ink); text-decoration: none; }
109
+ .card p { margin: 0 0 0.35rem; color: var(--muted); }
110
+ .meta { color: var(--muted-dim); font-size: 0.8rem; }
111
+ .empty { color: var(--muted); }
112
+
113
+ /* ── post page ── */
114
+ .post { margin-top: 2rem; }
115
+ .post h1 { font-size: clamp(1.8rem, 6vw, 2.4rem); font-weight: 800; }
116
+ .post .lede { margin: 0.75rem 0; }
117
+ .post .body { white-space: pre-line; margin-top: 1.5rem; color: var(--text); }
118
+ .pill {
119
+ font-size: 0.7rem;
120
+ font-weight: 700;
121
+ padding: 0.1rem 0.55rem;
122
+ border-radius: 999px;
123
+ vertical-align: middle;
124
+ }
125
+ .pill.draft { background: var(--spark); color: #000; }
126
+ .pill.live { background: #16a34a; color: #fff; }
127
+
128
+ /* ── admin ── */
129
+ .admin-head { display: flex; justify-content: space-between; align-items: center; }
130
+ .panel {
131
+ background: var(--surface);
132
+ border: 1px solid var(--border);
133
+ border-radius: var(--radius);
134
+ padding: 1.25rem;
135
+ margin: 0 0 1.25rem;
136
+ }
137
+ .panel h2 { margin-top: 0; font-size: 1.15rem; }
138
+ .panel h3 { margin: 1.25rem 0 0.5rem; font-size: 0.95rem; color: var(--muted); }
139
+ .row { display: flex; align-items: center; gap: 0.6rem; margin: 0.4rem 0; }
140
+ .grow { flex: 1; }
141
+ .done { text-decoration: line-through; color: var(--muted-dim); }
142
+ .hint { color: var(--muted); font-size: 0.85rem; }
143
+ .error { color: var(--danger); }
144
+
145
+ /* ── forms & buttons ── */
146
+ input, textarea {
147
+ font: inherit;
148
+ color: var(--text);
149
+ background: var(--bg);
150
+ border: 1px solid var(--border-strong);
151
+ border-radius: 8px;
152
+ padding: 0.5rem 0.7rem;
153
+ }
154
+ input:focus, textarea:focus { outline: none; border-color: var(--spark); }
155
+ .login { max-width: 380px; margin: 3rem auto; display: grid; gap: 0.75rem; }
156
+ .login label { display: grid; gap: 0.25rem; font-size: 0.9rem; color: var(--muted); }
157
+ .panel input, .panel textarea { width: 100%; margin: 0.25rem 0; }
158
+ .row input, .row button { width: auto; margin: 0; }
159
+
160
+ button {
161
+ font: inherit;
162
+ cursor: pointer;
163
+ color: var(--text);
164
+ background: var(--surface-2);
165
+ border: 1px solid var(--border-strong);
166
+ border-radius: 8px;
167
+ padding: 0.5rem 0.8rem;
168
+ transition: border-color 0.12s, color 0.12s, transform 0.08s;
169
+ }
170
+ button:hover:not(:disabled) { border-color: var(--spark); color: var(--spark-ink); }
171
+ button:active:not(:disabled) { transform: scale(0.97); }
172
+ button:disabled { opacity: 0.4; cursor: default; }
173
+ button.danger:hover:not(:disabled) { border-color: var(--danger); color: var(--danger); }
174
+ button.ghost, button.toggle { border: none; background: none; padding: 0.2rem; }
175
+ button.ghost:hover:not(:disabled), button.toggle:hover:not(:disabled) { color: var(--spark-ink); transform: none; }
@@ -0,0 +1,3 @@
1
+ {
2
+ "fonts": [{ "family": "JetBrains Mono", "google": true, "weights": [400, 500, 700, 800] }]
3
+ }
@@ -1,9 +0,0 @@
1
- <meta charset="utf-8">
2
- <meta name="viewport" content="width=device-width, initial-scale=1">
3
- <title>Not found</title>
4
- <link rel="stylesheet" href="/style.css">
5
- <article class="post" style="text-align:center; margin-top:4rem">
6
- <h1>404</h1>
7
- <p class="lede">This page took the day off.</p>
8
- <p><a href="/">← Back to the blog</a></p>
9
- </article>