kempo-server 1.10.5 → 1.10.6

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/README.md CHANGED
@@ -366,6 +366,12 @@ npm run tests:gui # Start the GUI test runner
366
366
  For advanced usage (filters, flags, GUI options), see:
367
367
  https://github.com/dustinpoissant/kempo-testing-framework
368
368
 
369
+ ## Single-Page Applications
370
+
371
+ Kempo Server makes it easy to serve SPAs by using `customRoutes` to redirect all HTML requests to your shell page while still serving individual page fragments from a `pages/` directory.
372
+
373
+ See **[SPA.md](./SPA.md)** for a full walkthrough.
374
+
369
375
  ## Documentation
370
376
 
371
377
  - **[Getting Started](./docs/getting-started.html)** - Installation and basic usage
package/SPA.md ADDED
@@ -0,0 +1,153 @@
1
+ # Single-Page Applications
2
+
3
+ Kempo Server supports single-page applications (SPAs) with no framework required. The approach uses `customRoutes` to redirect every HTML URL to one shell page, while individual page content lives as HTML fragments in a `pages/` directory. Client-side JavaScript handles navigation using the History API.
4
+
5
+ ## How It Works
6
+
7
+ 1. All top-level HTML requests (e.g. `/about.html`) are redirected to `app.html` via `customRoutes`.
8
+ 2. `app.html` is the shell — it contains the `<nav>`, a `<main>` content area, and loads `spa.js`.
9
+ 3. `spa.js` reads the current URL, fetches the matching fragment from `/pages/`, and injects it into `<main>`.
10
+ 4. Click events on `<a>` tags are intercepted to update the URL with `history.pushState` and load content without a full page reload.
11
+ 5. The `popstate` event handles browser back/forward navigation.
12
+
13
+ Requests to `/pages/*.html` are **not** redirected, so the fragments are still served directly by the server.
14
+
15
+ ## File Structure
16
+
17
+ ```
18
+ spa/
19
+ ├─ .config.json ← server config (routes + caching)
20
+ ├─ app.html ← shell page (loaded for every route)
21
+ ├─ spa.js ← client-side routing logic
22
+ └─ pages/
23
+ ├─ index.html ← home page fragment
24
+ ├─ page1.html
25
+ ├─ about.html
26
+ ├─ contact.html
27
+ └─ settings.html
28
+ ```
29
+
30
+ ## Configuration
31
+
32
+ Create a `.config.json` in your SPA root:
33
+
34
+ ```json
35
+ {
36
+ "customRoutes": {
37
+ "/kempo.css": "../node_modules/kempo-css/dist/kempo.min.css",
38
+ "/*.html": "./app.html",
39
+ "/": "./app.html"
40
+ },
41
+ "middleware": {
42
+ "security": {
43
+ "enabled": true,
44
+ "headers": {
45
+ "Cache-Control": "public, max-age=3600"
46
+ }
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ - `"/*.html": "./app.html"` — the `*` wildcard matches a single path segment, so it catches `/about.html` but not `/pages/about.html`.
53
+ - `"/": "./app.html"` — handles requests to the root.
54
+ - The `Cache-Control` header tells browsers to cache responses for 1 hour.
55
+
56
+ ## Shell Page (`app.html`)
57
+
58
+ The shell page is a minimal HTML document. Navigation links use absolute paths so they work regardless of the current URL. The `<main>` element is where page content gets injected.
59
+
60
+ ```html
61
+ <!doctype html>
62
+ <html lang="en">
63
+ <head>
64
+ <meta charset="UTF-8">
65
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
66
+ <title>My SPA</title>
67
+ <link rel="stylesheet" href="/kempo.css" />
68
+ </head>
69
+ <body>
70
+ <nav>
71
+ <a href="/">Home</a>
72
+ <a href="/about.html">About</a>
73
+ <a href="/contact.html">Contact</a>
74
+ </nav>
75
+ <main id="main"></main>
76
+ <script src="/spa.js"></script>
77
+ </body>
78
+ </html>
79
+ ```
80
+
81
+ ## Client-Side Router (`spa.js`)
82
+
83
+ ```javascript
84
+ const main = document.getElementById("main");
85
+
86
+ const loadPage = path => {
87
+ const page = (path === "/" || path === "/index.html")
88
+ ? "/pages/index.html"
89
+ : `/pages${path}`;
90
+ fetch(page)
91
+ .then(r => {
92
+ if(!r.ok) throw new Error(r.status);
93
+ return r.text();
94
+ })
95
+ .then(html => {
96
+ main.innerHTML = html;
97
+ })
98
+ .catch(() => {
99
+ main.innerHTML = "<h1>Page Not Found</h1>";
100
+ });
101
+ };
102
+
103
+ document.addEventListener("click", e => {
104
+ const a = e.target.closest("a");
105
+ if(!a || a.origin !== location.origin) return;
106
+ e.preventDefault();
107
+ history.pushState(null, "", a.href);
108
+ loadPage(a.pathname);
109
+ });
110
+
111
+ window.addEventListener("popstate", () => {
112
+ loadPage(location.pathname);
113
+ });
114
+
115
+ loadPage(location.pathname);
116
+ ```
117
+
118
+ ### How it works
119
+
120
+ - `loadPage` maps the URL pathname to a fragment file under `/pages/` and fetches it. The root path `/` maps to `/pages/index.html`.
121
+ - The `click` listener intercepts same-origin link clicks, prevents the default navigation, pushes the new URL into the browser history, and loads the content.
122
+ - The `popstate` listener handles back/forward button presses.
123
+ - On initial load, `loadPage(location.pathname)` renders the correct content for any URL — including direct navigation and browser refreshes.
124
+
125
+ ## Page Fragments
126
+
127
+ Each file inside `pages/` is a plain HTML fragment — just the content, no `<html>` or `<body>` wrapper needed:
128
+
129
+ ```html
130
+ <!-- pages/about.html -->
131
+ <h1>About</h1>
132
+ <p>This is an example single-page application powered by Kempo-Server.</p>
133
+ ```
134
+
135
+ ## Running the SPA
136
+
137
+ Add a script to your `package.json`:
138
+
139
+ ```json
140
+ {
141
+ "scripts": {
142
+ "spa": "kempo-server --root ./spa"
143
+ }
144
+ }
145
+ ```
146
+
147
+ Then run:
148
+
149
+ ```bash
150
+ npm run spa
151
+ ```
152
+
153
+ The server starts on `http://localhost:3000` by default. All `.html` URLs load `app.html` and client-side routing takes over from there.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kempo-server",
3
3
  "type": "module",
4
- "version": "1.10.5",
4
+ "version": "1.10.6",
5
5
  "description": "A lightweight, zero-dependency, file based routing server.",
6
6
  "main": "dist/index.js",
7
7
  "exports": {
@@ -15,6 +15,7 @@
15
15
  "scripts": {
16
16
  "build": "node scripts/build.js",
17
17
  "docs": "node dist/index.js -r ./docs",
18
+ "spa": "node dist/index.js -r ./spa",
18
19
  "test": "npx kempo-test",
19
20
  "test:gui": "npx kempo-test --gui",
20
21
  "test:browser": "npx kempo-test -b",
@@ -0,0 +1,15 @@
1
+ {
2
+ "customRoutes": {
3
+ "/kempo.css": "../node_modules/kempo-css/dist/kempo.min.css",
4
+ "/*.html": "./app.html",
5
+ "/": "./app.html"
6
+ },
7
+ "middleware": {
8
+ "security": {
9
+ "enabled": true,
10
+ "headers": {
11
+ "Cache-Control": "public, max-age=3600"
12
+ }
13
+ }
14
+ }
15
+ }
package/spa/app.html ADDED
@@ -0,0 +1,20 @@
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>Kempo-Server SPA</title>
7
+ <link rel="stylesheet" href="/kempo.css" />
8
+ </head>
9
+ <body>
10
+ <nav>
11
+ <a href="/">Home</a>
12
+ <a href="/page1.html">Page 1</a>
13
+ <a href="/about.html">About</a>
14
+ <a href="/contact.html">Contact</a>
15
+ <a href="/settings.html">Settings</a>
16
+ </nav>
17
+ <main id="main"></main>
18
+ <script src="/spa.js"></script>
19
+ </body>
20
+ </html>
@@ -0,0 +1,2 @@
1
+ <h1>About</h1>
2
+ <p>This is an example single-page application powered by Kempo-Server.</p>
@@ -0,0 +1,2 @@
1
+ <h1>Contact</h1>
2
+ <p>Get in touch at <a href="mailto:hello@example.com">hello@example.com</a>.</p>
@@ -0,0 +1 @@
1
+ <h1>Hello <code>index.html</code></h1>
@@ -0,0 +1 @@
1
+ <h1>Hello <code>page1.html</code></h1>
@@ -0,0 +1,2 @@
1
+ <h1>Settings</h1>
2
+ <p>Configure your preferences here.</p>
package/spa/spa.js ADDED
@@ -0,0 +1,32 @@
1
+ const main = document.getElementById("main");
2
+
3
+ const loadPage = path => {
4
+ const page = (path === "/" || path === "/index.html")
5
+ ? "/pages/index.html"
6
+ : `/pages${path}`;
7
+ fetch(page)
8
+ .then(r => {
9
+ if(!r.ok) throw new Error(r.status);
10
+ return r.text();
11
+ })
12
+ .then(html => {
13
+ main.innerHTML = html;
14
+ })
15
+ .catch(() => {
16
+ main.innerHTML = "<h1>Page Not Found</h1>";
17
+ });
18
+ };
19
+
20
+ document.addEventListener("click", e => {
21
+ const a = e.target.closest("a");
22
+ if(!a || a.origin !== location.origin) return;
23
+ e.preventDefault();
24
+ history.pushState(null, "", a.href);
25
+ loadPage(a.pathname);
26
+ });
27
+
28
+ window.addEventListener("popstate", () => {
29
+ loadPage(location.pathname);
30
+ });
31
+
32
+ loadPage(location.pathname);