create-spark-html-app 0.10.0 → 0.11.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/package.json +1 -1
- package/template-ssr/README.md +33 -17
- package/template-ssr/components/nav.html +6 -5
- package/template-ssr/components/todo-list.html +6 -1
- package/template-ssr/package.json +2 -1
- package/template-ssr/pages/_layout.html +14 -0
- package/template-ssr/pages/about.html +2 -7
- package/template-ssr/pages/admin/index.html +20 -25
- package/template-ssr/pages/blog/[slug].html +4 -8
- package/template-ssr/pages/index.html +4 -9
- package/template-ssr/pages/login.html +19 -0
- package/template-ssr/seed/posts.json +38 -0
- package/template-ssr/seed/todos.json +5 -0
- package/template-ssr/seed/users.json +8 -0
- package/template-ssr/components/login-form.html +0 -38
- package/template-ssr/components/logout-button.html +0 -8
- package/template-ssr/setup.js +0 -76
package/package.json
CHANGED
package/template-ssr/README.md
CHANGED
|
@@ -2,55 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
A complete blog: public posts with SEO titles/meta, dynamic routes, an
|
|
4
4
|
about page, and an auth-gated admin panel where the author manages posts —
|
|
5
|
-
and a private todo list. All of it is a handful of `.html`
|
|
6
|
-
|
|
5
|
+
and a live-updating private todo list. All of it is a handful of `.html`
|
|
6
|
+
files and one SQLite database. **No build step. No setup script. No fetch
|
|
7
|
+
calls for the basics.**
|
|
7
8
|
|
|
8
9
|
## Run it
|
|
9
10
|
|
|
10
11
|
```bash
|
|
11
12
|
bun install
|
|
12
|
-
bun run dev # seeds
|
|
13
|
+
bun run dev # creates + seeds the DB from the templates, serves on :3000
|
|
13
14
|
```
|
|
14
15
|
|
|
15
|
-
Sign in at [/
|
|
16
|
-
**me@spark-html.com** / **spark
|
|
16
|
+
Sign in at [/login](http://localhost:3000/login) with
|
|
17
|
+
**me@spark-html.com** / **spark** — a plain form; it works with JavaScript
|
|
18
|
+
disabled.
|
|
17
19
|
|
|
18
20
|
## What's demonstrated where
|
|
19
21
|
|
|
20
22
|
| Feature | Where |
|
|
21
23
|
|---|---|
|
|
24
|
+
| Layouts | `pages/_layout.html` — nav, styles, `app.js` and the `author` query, once for every page |
|
|
22
25
|
| Filesystem routing | `pages/index.html` → `/`, `pages/about.html` → `/about` |
|
|
23
26
|
| Dynamic routes | `pages/blog/[slug].html` → `/blog/:slug` — `:slug` binds into the SQL |
|
|
24
|
-
|
|
|
25
|
-
|
|
|
27
|
+
| Named page data | `posts = SELECT …` — the variable is named in the block, no endpoint exposed |
|
|
28
|
+
| Real 404s | `<template else status="404">` on the missing-post branch |
|
|
29
|
+
| Guards | `/admin`: `<spark-ssr guard="session" redirect="/login" />` — one line |
|
|
30
|
+
| No-JS forms | `pages/login.html` and the logout button — plain forms, 303 back |
|
|
31
|
+
| Form validation | the login form's `required`/`type="email"` run server-side too |
|
|
32
|
+
| The template is the schema | `seed/*.json` + `<spark-ssr table="…" seed="…">` — tables created and seeded at startup; `bun run db` shows the diff |
|
|
33
|
+
| Live updates | `<spark-ssr table="todos" … live />` — open /admin in two tabs |
|
|
34
|
+
| Per-page `<title>`/`<meta>` | literal tags at the top of each page, `{expr}`-interpolated |
|
|
35
|
+
| SEO | og:title/og:description derive from the head; `/sitemap.xml` enumerates `/blog/:slug` from its query; `/robots.txt` honors the admin page's `noindex` |
|
|
26
36
|
| Server → component props | `index.html` passes each whole `post` row into `post-card.html` |
|
|
27
|
-
|
|
|
28
|
-
| Client components | `login-form`, `post-editor`, `todo-list` — their `<script>` runs in the browser |
|
|
37
|
+
| Client components | `post-editor`, `todo-list` — their `<script>` runs in the browser |
|
|
29
38
|
| Auth | `spark.json` `"auth"` → sessions, `POST /api/users?auth` login, hashed passwords |
|
|
30
39
|
| Auth-scoped CRUD | `posts` and `todos` carry `user_id` → their APIs are session-scoped (401 anonymous) |
|
|
31
40
|
| Draft privacy | the `[slug]` query: `published = 1 OR :session.id IS NOT NULL` — authors preview drafts |
|
|
32
41
|
| Middleware | `middleware.html` disables public signups (single-author blog) |
|
|
33
42
|
| Aggregates | `about.html`'s `COUNT(*)` serves an object → `{stats.n}` |
|
|
34
|
-
| Custom error page | `404.html` |
|
|
35
43
|
| Dark/light theme | spark-html-theme — `app.js` one-liner + `theme-toggle.html`; no-flash init is auto-inlined |
|
|
36
|
-
| Fonts | spark.json `"fonts"` → spark-html-font tags in every `<head>`
|
|
37
|
-
| Images | `bun run build` runs spark-html-image over `dist/` — webp + srcset
|
|
44
|
+
| Fonts | spark.json `"fonts"` → spark-html-font tags in every `<head>` |
|
|
45
|
+
| Images | `bun run build` runs spark-html-image over `dist/` — webp + srcset |
|
|
38
46
|
|
|
39
47
|
## The mental model
|
|
40
48
|
|
|
49
|
+
- `pages/_layout.html` wraps every page in the folder; `<slot>` is the page.
|
|
50
|
+
Its `<spark-ssr>` vars are in scope everywhere it wraps.
|
|
41
51
|
- A **page**'s plain `<script>` runs on the **server** (it's the escape
|
|
42
52
|
hatch); `<script type="module">`/`src` scripts ship to the browser.
|
|
43
53
|
- A **component**'s `<script>` runs in the **browser**. Components are
|
|
44
54
|
otherwise pure UI: they render the props they're given.
|
|
45
|
-
- `<spark-ssr>` declares data.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
- `<spark-ssr>` declares data. `var = SELECT …` names page data;
|
|
56
|
+
`table="…"` gives you scoped REST CRUD (+ `seed`, `live`, `limit`,
|
|
57
|
+
`search`); `guard="…"` protects the page.
|
|
58
|
+
- Plain forms to `/api/*` work without JavaScript — success 303s back,
|
|
59
|
+
the markup's constraint attributes validate on the server.
|
|
60
|
+
- Everything hot-reloads — pages, layouts, components, queries, middleware
|
|
61
|
+
— and dev errors land on the page, not in a bare 500. Open
|
|
62
|
+
[/__spark/plan](http://localhost:3000/__spark/plan) to see the inferred
|
|
63
|
+
backend.
|
|
49
64
|
|
|
50
65
|
## Deploy
|
|
51
66
|
|
|
52
67
|
```bash
|
|
53
|
-
bun run build
|
|
68
|
+
bun run build # dist/ + compiled binary; images optimized
|
|
69
|
+
bun spark-ssr build --docker # …plus a Dockerfile
|
|
54
70
|
PORT=3000 ./dist/app
|
|
55
71
|
```
|
|
56
72
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
<!-- Pure UI — no <script>. Props (blog,
|
|
2
|
-
|
|
1
|
+
<!-- Pure UI — no <script>. Props (blog, path) come from the layout; `path`
|
|
2
|
+
is ambient page scope (the current request path), so the highlight is
|
|
3
|
+
right on the server render and after client mount alike. -->
|
|
3
4
|
<nav class="nav">
|
|
4
5
|
<a class="brand" href="/">⚡ {blog}</a>
|
|
5
6
|
<span class="links">
|
|
6
|
-
<a href="/" :class="
|
|
7
|
-
<a href="/about" :class="
|
|
8
|
-
<a href="/admin" :class="
|
|
7
|
+
<a href="/" :class="path === '/' ? 'on' : ''">Posts</a>
|
|
8
|
+
<a href="/about" :class="path === '/about' ? 'on' : ''">About</a>
|
|
9
|
+
<a href="/admin" :class="path.startsWith('/admin') ? 'on' : ''">Manage</a>
|
|
9
10
|
<span import="/components/theme-toggle"></span>
|
|
10
11
|
</span>
|
|
11
12
|
</nav>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<!-- The author's private todos. Same auto-CRUD pattern as posts — the
|
|
2
2
|
user_id column scopes every row to the session, so nobody else can
|
|
3
|
-
read or touch them (try /api/todos signed out: 401).
|
|
3
|
+
read or touch them (try /api/todos signed out: 401). The todos block
|
|
4
|
+
declares `live`, so writes ping /__spark/live: the two lines at the
|
|
5
|
+
bottom keep every open tab (or the author's phone) in sync. -->
|
|
4
6
|
<section class="panel">
|
|
5
7
|
<h2>Private todos</h2>
|
|
6
8
|
<div class="row">
|
|
@@ -48,4 +50,7 @@
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
refresh();
|
|
53
|
+
|
|
54
|
+
const es = new EventSource('/__spark/live');
|
|
55
|
+
es.onmessage = (e) => { if (e.data === 'todos') refresh(); };
|
|
51
56
|
</script>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!-- The layout: every page in pages/ renders inside it — <slot> is the page.
|
|
2
|
+
Its <spark-ssr> vars (author) are in scope for every page it wraps, and
|
|
3
|
+
its head tags lift into <head> too (a page's own tags win on conflict).
|
|
4
|
+
The three lines every page used to repeat live here, once. -->
|
|
5
|
+
<link rel="stylesheet" href="/style.css">
|
|
6
|
+
<script type="module" src="/app.js"></script>
|
|
7
|
+
|
|
8
|
+
<div import="/components/nav" blog="{author.name}" path="{path}"></div>
|
|
9
|
+
|
|
10
|
+
<slot></slot>
|
|
11
|
+
|
|
12
|
+
<spark-ssr>
|
|
13
|
+
author = SELECT id, name, email, bio FROM users LIMIT 1
|
|
14
|
+
</spark-ssr>
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
<!-- /about —
|
|
1
|
+
<!-- /about — {author} comes from the layout; this page adds one aggregate:
|
|
2
2
|
a COUNT(*) query serves an object, so {stats.n} just works. -->
|
|
3
3
|
<title>About {author.name}</title>
|
|
4
4
|
<meta name="description" content="Who is {author.name} — {author.bio}">
|
|
5
|
-
<link rel="stylesheet" href="/style.css">
|
|
6
|
-
<script type="module" src="/app.js"></script>
|
|
7
|
-
|
|
8
|
-
<div import="/components/nav" blog="{author.name}" active="about"></div>
|
|
9
5
|
|
|
10
6
|
<article class="post">
|
|
11
7
|
<img class="avatar" src="/img/avatar.png" alt="{author.name}" width="88" height="88">
|
|
@@ -17,6 +13,5 @@
|
|
|
17
13
|
</article>
|
|
18
14
|
|
|
19
15
|
<spark-ssr>
|
|
20
|
-
|
|
21
|
-
GET /api/stats → SELECT COUNT(*) AS n FROM posts WHERE published = 1
|
|
16
|
+
stats = SELECT COUNT(*) AS n FROM posts WHERE published = 1
|
|
22
17
|
</spark-ssr>
|
|
@@ -1,30 +1,25 @@
|
|
|
1
|
-
<!-- /admin —
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
<!-- /admin — one guard line protects the whole page: signed out → 303 to
|
|
2
|
+
/login. The table blocks give the components their scoped REST APIs
|
|
3
|
+
(posts + todos carry user_id → session-scoped, 401 for anyone else);
|
|
4
|
+
the seed="…" files define AND fill the tables on first run — there is
|
|
5
|
+
no setup.js — and `live` keeps every open todos tab in sync. -->
|
|
5
6
|
<title>Manage · {author.name}</title>
|
|
6
7
|
<meta name="robots" content="noindex">
|
|
7
|
-
<link rel="stylesheet" href="/style.css">
|
|
8
|
-
<script type="module" src="/app.js"></script>
|
|
9
8
|
|
|
10
|
-
<
|
|
9
|
+
<spark-ssr guard="session" redirect="/login" />
|
|
11
10
|
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<div import="/components/login-form"></div>
|
|
24
|
-
</template>
|
|
11
|
+
<main class="admin">
|
|
12
|
+
<header class="admin-head">
|
|
13
|
+
<p>Signed in as <strong>{session.email}</strong></p>
|
|
14
|
+
<!-- Logout is a plain form too — works with JavaScript disabled. -->
|
|
15
|
+
<form action="/api/logout" method="post" redirect="/">
|
|
16
|
+
<button class="ghost">Sign out</button>
|
|
17
|
+
</form>
|
|
18
|
+
</header>
|
|
19
|
+
<div import="/components/post-editor"></div>
|
|
20
|
+
<div import="/components/todo-list"></div>
|
|
21
|
+
</main>
|
|
25
22
|
|
|
26
|
-
<spark-ssr table="
|
|
27
|
-
<spark-ssr table="
|
|
28
|
-
<spark-ssr
|
|
29
|
-
GET /api/author → SELECT id, name, email, bio FROM users LIMIT 1
|
|
30
|
-
</spark-ssr>
|
|
23
|
+
<spark-ssr table="users" seed="./seed/users.json" />
|
|
24
|
+
<spark-ssr table="posts" seed="./seed/posts.json" />
|
|
25
|
+
<spark-ssr table="todos" seed="./seed/todos.json" live />
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
<!-- /blog/:slug — the filename IS the route; :slug binds into the query.
|
|
2
|
-
|
|
2
|
+
A missing row is a REAL 404 (the status= on the rendered branch), and
|
|
3
|
+
drafts stay invisible to anonymous readers while the signed-in author
|
|
3
4
|
gets a live preview (the OR :session.id clause). -->
|
|
4
5
|
<title>{post ? post.title : 'Not found'} · {author.name}</title>
|
|
5
6
|
<meta name="description" content="{post ? post.excerpt : 'This post does not exist.'}">
|
|
6
|
-
<link rel="stylesheet" href="/style.css">
|
|
7
|
-
<script type="module" src="/app.js"></script>
|
|
8
|
-
|
|
9
|
-
<div import="/components/nav" blog="{author.name}"></div>
|
|
10
7
|
|
|
11
8
|
<template if="post">
|
|
12
9
|
<article class="post">
|
|
@@ -19,7 +16,7 @@
|
|
|
19
16
|
<div class="body">{post.body}</div>
|
|
20
17
|
</article>
|
|
21
18
|
</template>
|
|
22
|
-
<template else>
|
|
19
|
+
<template else status="404">
|
|
23
20
|
<article class="post">
|
|
24
21
|
<h1>Not found</h1>
|
|
25
22
|
<p>That post doesn't exist — or it's a draft and you're not signed in.</p>
|
|
@@ -28,8 +25,7 @@
|
|
|
28
25
|
</template>
|
|
29
26
|
|
|
30
27
|
<spark-ssr>
|
|
31
|
-
|
|
32
|
-
GET /api/blog/[slug] → SELECT * FROM posts
|
|
28
|
+
post = SELECT * FROM posts
|
|
33
29
|
WHERE slug = :slug AND (published = 1 OR :session.id IS NOT NULL)
|
|
34
30
|
LIMIT 1
|
|
35
31
|
</spark-ssr>
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
<!-- The homepage.
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<!-- The homepage. The layout owns the chrome and provides {author}; this
|
|
2
|
+
page declares only what's unique to it. `posts = SELECT …` is page data
|
|
3
|
+
with NO public endpoint — name the variable, skip the route. -->
|
|
4
4
|
<title>{author.name} — a spark blog</title>
|
|
5
5
|
<meta name="description" content="{author.bio}">
|
|
6
|
-
<link rel="stylesheet" href="/style.css">
|
|
7
|
-
<script type="module" src="/app.js"></script>
|
|
8
|
-
|
|
9
|
-
<div import="/components/nav" blog="{author.name}" active="home"></div>
|
|
10
6
|
|
|
11
7
|
<header class="hero">
|
|
12
8
|
<img class="avatar" src="/img/avatar.png" alt="{author.name}" width="88" height="88">
|
|
@@ -28,6 +24,5 @@
|
|
|
28
24
|
</main>
|
|
29
25
|
|
|
30
26
|
<spark-ssr>
|
|
31
|
-
|
|
32
|
-
GET /api/published → SELECT * FROM posts WHERE published = 1 ORDER BY created_at DESC
|
|
27
|
+
posts = SELECT * FROM posts WHERE published = 1 ORDER BY created_at DESC
|
|
33
28
|
</spark-ssr>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!-- Sign in with a PLAIN FORM — no JavaScript required. The server answers
|
|
2
|
+
a browser like a browser: success 303s to redirect="/admin"; a wrong
|
|
3
|
+
password re-renders this page with {errors} in scope. The `required`
|
|
4
|
+
attributes are enforced server-side too — the form is the validator. -->
|
|
5
|
+
<title>Sign in · {author.name}</title>
|
|
6
|
+
<meta name="robots" content="noindex">
|
|
7
|
+
|
|
8
|
+
<form class="panel login" action="/api/users?auth" method="post" redirect="/admin">
|
|
9
|
+
<h2>Author sign-in</h2>
|
|
10
|
+
<label>Email
|
|
11
|
+
<input type="email" name="email" autocomplete="username" required>
|
|
12
|
+
</label>
|
|
13
|
+
<label>Password
|
|
14
|
+
<input type="password" name="password" autocomplete="current-password" required>
|
|
15
|
+
</label>
|
|
16
|
+
<button>Sign in</button>
|
|
17
|
+
<template if="errors"><p class="error">Wrong email or password.</p></template>
|
|
18
|
+
<p class="hint">Seeded account: me@spark-html.com / spark</p>
|
|
19
|
+
</form>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"user_id": 1,
|
|
4
|
+
"slug": "hello-spark",
|
|
5
|
+
"title": "Hello, Spark",
|
|
6
|
+
"excerpt": "Why this blog runs on HTML that infers its own backend.",
|
|
7
|
+
"body": "This site is a handful of .html files and one SQLite database.\n\nEach page declares the data it needs in a <spark-ssr> block, and the server infers the rest: routes from filenames, APIs from SQL, auth from a user_id column — even these tables and rows came from seed files the templates point at.\n\nView source on any page — what you see is what I wrote.",
|
|
8
|
+
"published": 1,
|
|
9
|
+
"created_at": "2026-06-21"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"user_id": 1,
|
|
13
|
+
"slug": "html-that-reacts",
|
|
14
|
+
"title": "HTML that reacts",
|
|
15
|
+
"excerpt": "Templates, loops and conditionals — in plain HTML attributes.",
|
|
16
|
+
"body": "A <template each=\"post in posts\"> renders a list. A <template if> branches. {curly} braces interpolate.\n\nOn the server they render to static HTML; in the browser the same syntax comes alive as components. One mental model, both sides of the wire.",
|
|
17
|
+
"published": 1,
|
|
18
|
+
"created_at": "2026-06-27"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"user_id": 1,
|
|
22
|
+
"slug": "zero-config-ssr",
|
|
23
|
+
"title": "Zero-config SSR",
|
|
24
|
+
"excerpt": "Filesystem routing, layouts, sessions, live updates — from the template.",
|
|
25
|
+
"body": "pages/blog/[slug].html serves /blog/:slug, and :slug binds straight into the page query. pages/_layout.html wraps every page — nav, styles, footer, once.\n\nDeclare <spark-ssr table=\"todos\" live /> and the REST endpoints exist, the table is created from the template, and every open tab stays in sync. Delete the file and it all goes away.",
|
|
26
|
+
"published": 1,
|
|
27
|
+
"created_at": "2026-07-02"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"user_id": 1,
|
|
31
|
+
"slug": "drafts-are-private",
|
|
32
|
+
"title": "Drafts are private",
|
|
33
|
+
"excerpt": "This post is unpublished — only the signed-in author sees it.",
|
|
34
|
+
"body": "The page query keeps drafts out of anonymous requests:\n\nWHERE slug = :slug AND (published = 1 OR :session.id IS NOT NULL)\n\nAnd a missing row is a REAL 404 now — <template else status=\"404\"> sets the response status, so crawlers never index an empty page. Sign in at /admin and this post appears.",
|
|
35
|
+
"published": 0,
|
|
36
|
+
"created_at": "2026-07-04"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
<!-- A component <script> runs in the BROWSER (pages' plain <script> runs on
|
|
2
|
-
the server — that's the split). POST /api/users?auth is the login
|
|
3
|
-
endpoint spark-ssr derives from the auth config. -->
|
|
4
|
-
<form class="panel login" onsubmit={login}>
|
|
5
|
-
<h2>Author sign-in</h2>
|
|
6
|
-
<label>Email
|
|
7
|
-
<input type="email" bind:value="email" autocomplete="username" required>
|
|
8
|
-
</label>
|
|
9
|
-
<label>Password
|
|
10
|
-
<input type="password" bind:value="password" autocomplete="current-password" required>
|
|
11
|
-
</label>
|
|
12
|
-
<button :disabled="pending">{pending ? 'Signing in…' : 'Sign in'}</button>
|
|
13
|
-
<template if="error"><p class="error">{error}</p></template>
|
|
14
|
-
<p class="hint">Seeded account: me@spark-html.com / spark</p>
|
|
15
|
-
</form>
|
|
16
|
-
|
|
17
|
-
<script>
|
|
18
|
-
let email = '';
|
|
19
|
-
let password = '';
|
|
20
|
-
let error = '';
|
|
21
|
-
let pending = false;
|
|
22
|
-
|
|
23
|
-
async function login() {
|
|
24
|
-
pending = true;
|
|
25
|
-
error = '';
|
|
26
|
-
const res = await fetch('/api/users?auth', {
|
|
27
|
-
method: 'POST',
|
|
28
|
-
headers: { 'content-type': 'application/json' },
|
|
29
|
-
body: JSON.stringify({ email, password }),
|
|
30
|
-
});
|
|
31
|
-
if (res.ok) {
|
|
32
|
-
location.reload();
|
|
33
|
-
} else {
|
|
34
|
-
error = 'Wrong email or password.';
|
|
35
|
-
pending = false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
</script>
|
package/template-ssr/setup.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// One-time (idempotent) dev database setup — `bun run dev` runs it for you.
|
|
2
|
-
// Swap spark.json's db to postgres:// any time; no code changes needed.
|
|
3
|
-
import { Database } from 'bun:sqlite';
|
|
4
|
-
|
|
5
|
-
const db = new Database('./dev.db', { create: true });
|
|
6
|
-
|
|
7
|
-
db.run(`CREATE TABLE IF NOT EXISTS users (
|
|
8
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9
|
-
email TEXT NOT NULL UNIQUE,
|
|
10
|
-
password TEXT NOT NULL,
|
|
11
|
-
name TEXT NOT NULL,
|
|
12
|
-
bio TEXT DEFAULT ''
|
|
13
|
-
)`);
|
|
14
|
-
|
|
15
|
-
db.run(`CREATE TABLE IF NOT EXISTS posts (
|
|
16
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
17
|
-
user_id INTEGER NOT NULL,
|
|
18
|
-
slug TEXT NOT NULL UNIQUE,
|
|
19
|
-
title TEXT NOT NULL,
|
|
20
|
-
excerpt TEXT DEFAULT '',
|
|
21
|
-
body TEXT DEFAULT '',
|
|
22
|
-
published INTEGER DEFAULT 0,
|
|
23
|
-
created_at TEXT DEFAULT (date('now'))
|
|
24
|
-
)`);
|
|
25
|
-
|
|
26
|
-
db.run(`CREATE TABLE IF NOT EXISTS todos (
|
|
27
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
-
user_id INTEGER NOT NULL,
|
|
29
|
-
title TEXT NOT NULL,
|
|
30
|
-
done INTEGER DEFAULT 0
|
|
31
|
-
)`);
|
|
32
|
-
|
|
33
|
-
if (db.query('SELECT COUNT(*) AS n FROM users').get().n === 0) {
|
|
34
|
-
// The author account — sign in at /admin to manage the blog.
|
|
35
|
-
const hash = await Bun.password.hash('spark');
|
|
36
|
-
db.run('INSERT INTO users (email, password, name, bio) VALUES (?, ?, ?, ?)', [
|
|
37
|
-
'me@spark-html.com',
|
|
38
|
-
hash,
|
|
39
|
-
'Ada Spark',
|
|
40
|
-
'I write HTML that reacts. No compiler, no virtual DOM, no build step — just the platform, with a spark.',
|
|
41
|
-
]);
|
|
42
|
-
|
|
43
|
-
const posts = [
|
|
44
|
-
['hello-spark', 'Hello, Spark',
|
|
45
|
-
'Why this blog runs on HTML that infers its own backend.',
|
|
46
|
-
'This site is a handful of .html files and one SQLite database.\n\nEach page declares the data it needs in a <spark-ssr> block, and the server infers the rest: routes from filenames, APIs from SQL, auth from a user_id column.\n\nView source on any page — what you see is what I wrote.',
|
|
47
|
-
1, '2026-06-21'],
|
|
48
|
-
['html-that-reacts', 'HTML that reacts',
|
|
49
|
-
'Templates, loops and conditionals — in plain HTML attributes.',
|
|
50
|
-
'A <template each="post in posts"> renders a list. A <template if> branches. {curly} braces interpolate.\n\nOn the server they render to static HTML; in the browser the same syntax comes alive as components. One mental model, both sides of the wire.',
|
|
51
|
-
1, '2026-06-27'],
|
|
52
|
-
['zero-config-ssr', 'Zero-config SSR',
|
|
53
|
-
'Filesystem routing, sessions, uploads and CRUD — from the template.',
|
|
54
|
-
'pages/blog/[slug].html serves /blog/:slug, and :slug binds straight into the page query.\n\nDeclare <spark-ssr table="todos"> and the REST endpoints exist. Add a user_id column and they are scoped to the signed-in user. Delete the file and it all goes away.',
|
|
55
|
-
1, '2026-07-02'],
|
|
56
|
-
['drafts-are-private', 'Drafts are private',
|
|
57
|
-
'This post is unpublished — only the signed-in author sees it.',
|
|
58
|
-
'The page query keeps drafts out of anonymous requests:\n\nWHERE slug = :slug AND (published = 1 OR :session.id IS NOT NULL)\n\nSign in at /admin and this post appears on the homepage preview and here. Publish it from the admin panel when it is ready.',
|
|
59
|
-
0, '2026-07-04'],
|
|
60
|
-
];
|
|
61
|
-
for (const [slug, title, excerpt, body, published, at] of posts) {
|
|
62
|
-
db.run(
|
|
63
|
-
'INSERT INTO posts (user_id, slug, title, excerpt, body, published, created_at) VALUES (1, ?, ?, ?, ?, ?, ?)',
|
|
64
|
-
[slug, title, excerpt, body, published, at],
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
db.run(`INSERT INTO todos (user_id, title, done) VALUES
|
|
69
|
-
(1, 'Finish the drafts post', 1),
|
|
70
|
-
(1, 'Write about companion packages', 0),
|
|
71
|
-
(1, 'Reply to reader mail', 0)`);
|
|
72
|
-
|
|
73
|
-
console.log('⚡ seeded dev.db — sign in at /admin with me@spark-html.com / spark');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
db.close();
|