kempo-server 3.0.4 → 3.0.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.
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -73,11 +73,11 @@
73
73
 
74
74
  </menu>
75
75
  </k-aside>
76
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
77
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
78
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
79
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
80
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
77
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
78
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
79
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
80
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
81
81
  <script>
82
82
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
83
83
  await window.customElements.whenDefined('k-aside');
@@ -102,28 +102,33 @@
102
102
 
103
103
  <p>Kempo Server includes a built-in XML-based templating system for building static sites with shared layouts, reusable fragments, and dynamic content. Pages can be pre-rendered at build time or server-side rendered on each request.</p>
104
104
 
105
- <nav class="b r mb p">
106
- <h4 class="mt0">On This Page</h4>
107
- <ul>
108
- <li><a href="#overview">Overview</a></li>
109
- <li><a href="#file-types">File Types</a></li>
110
- <li><a href="#templates">Templates</a></li>
111
- <li><a href="#pages">Pages</a>
112
- <ul>
113
- <li><a href="#frontmatter">Frontmatter</a></li>
114
- </ul>
115
- </li>
116
- <li><a href="#fragments">Fragments</a></li>
117
- <li><a href="#global-files">Global Files</a></li>
118
- <li><a href="#fragments-vs-globals">Fragments vs. Global Files</a></li>
119
- <li><a href="#variables">Variables</a></li>
120
- <li><a href="#conditionals">Conditionals</a></li>
121
- <li><a href="#loops">Loops</a></li>
122
- <li><a href="#rendering">Rendering</a></li>
123
- <li><a href="#ssr">Server-Side Rendering</a></li>
124
- <li><a href="#configuration">Configuration</a></li>
125
- </ul>
126
- </nav>
105
+ <k-accordion persistent-id="templating-toc" class="mb">
106
+ <k-accordion-header for-panel="toc">Table of Contents</k-accordion-header>
107
+ <k-accordion-panel name="toc">
108
+ <ul class="m">
109
+ <li><a href="#overview">Overview</a></li>
110
+ <li><a href="#templates">Templates</a></li>
111
+ <li><a href="#pages">Pages</a>
112
+ <ul>
113
+ <li><a href="#frontmatter">Frontmatter</a></li>
114
+ </ul>
115
+ </li>
116
+ <li><a href="#fragments">Fragments</a></li>
117
+ <li><a href="#global-files">Global Files</a></li>
118
+ <li><a href="#fragments-vs-globals">Fragments vs. Global Files</a></li>
119
+ <li><a href="#template-syntax">Template Syntax</a>
120
+ <ul>
121
+ <li><a href="#variables">Variables</a></li>
122
+ <li><a href="#conditionals">Conditionals</a></li>
123
+ <li><a href="#loops">Loops</a></li>
124
+ </ul>
125
+ </li>
126
+ <li><a href="#rendering">Rendering</a></li>
127
+ <li><a href="#ssr">Server-Side Rendering</a></li>
128
+ <li><a href="#configuration">Configuration</a></li>
129
+ </ul>
130
+ </k-accordion-panel>
131
+ </k-accordion>
127
132
 
128
133
  <h2 id="overview">Overview</h2>
129
134
  <p>The templating system uses four file types that work together:</p>
@@ -133,12 +138,23 @@
133
138
  <li><strong>Fragments</strong> (<code>*.fragment.html</code>) &mdash; Reusable HTML partials included in templates or other fragments</li>
134
139
  <li><strong>Globals</strong> (<code>*.global.html</code>) &mdash; Site-wide content blocks automatically injected into every page render</li>
135
140
  </ul>
136
- <p>All three file types are blocked from being served directly by the default <code>disallowedRegex</code> configuration.</p>
137
-
138
- <h2 id="file-types">File Types</h2>
139
141
  <p>A typical project structure:</p>
140
142
  <pre><code class="hljs markdown">my-site/<br />├─ default.template.html # Shared layout<br />├─ nav.fragment.html # Reusable navigation<br />├─ footer.fragment.html # Reusable footer<br />├─ analytics.global.html # Site-wide scripts injected into every page<br />├─ index.page.html # Homepage → renders to index.html<br />├─ about.page.html # About → renders to about.html<br />├─ blog/<br />│ ├─ index.page.html # Blog index → blog/index.html<br />│ ├─ post-1.page.html # Blog post → blog/post-1.html<br /></code></pre>
141
- <p>Templates and fragments are resolved by walking up the directory tree from the page file to the root, so subdirectories can override them by providing their own versions.</p>
143
+ <p>All four file types are blocked from being served directly by the default <code>disallowedRegex</code> configuration. Each type is located differently at startup:</p>
144
+ <div class="table-wrapper mb">
145
+ <table>
146
+ <thead>
147
+ <tr><th>File Type</th><th>How It Is Found</th></tr>
148
+ </thead>
149
+ <tbody>
150
+ <tr><td><code>*.page.html</code></td><td>Recursive scan of the entire root directory tree &mdash; pages in any subdirectory are discovered automatically.</td></tr>
151
+ <tr><td><code>*.global.html</code></td><td>Recursive scan of the entire root directory tree &mdash; globals in any subdirectory are merged together automatically.</td></tr>
152
+ <tr><td><code>*.template.html</code></td><td>Walks <strong>up</strong> the directory tree from the page file&rsquo;s directory toward the root. The nearest match wins. A subdirectory template overrides the root one for pages below it.</td></tr>
153
+ <tr><td><code>*.fragment.html</code></td><td>Same up-walk as templates &mdash; starts at the including file&rsquo;s directory and searches each ancestor up to the root. Nearest match wins.</td></tr>
154
+ </tbody>
155
+ </table>
156
+ </div>
157
+ <p>Because pages and globals use a downward recursive scan, you can freely organize them into subdirectories and they will still be found. Templates and fragments use an upward walk, so they must live in the same directory as the files that reference them, or in a parent directory. Placing a template or fragment in a sibling or cousin directory will not be found.</p>
142
158
 
143
159
  <h2 id="templates">Templates</h2>
144
160
  <p>A template defines the shared HTML structure for your pages. Use <code>&lt;location&gt;</code> tags to define named content slots that pages will fill.</p>
@@ -169,13 +185,15 @@
169
185
  description: Overview of the routing system
170
186
  --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">page</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Routing"</span>&gt;</span><br /> ...<br /><span class="hljs-tag">&lt;/<span class="hljs-name">page</span>&gt;</span></code></pre>
171
187
  <p>The comment is purely for the author &mdash; nothing before <code>&lt;page&gt;</code> is parsed or rendered.</p>
188
+
189
+ <h2 id="fragments">Fragments</h2>
172
190
  <p>Fragments are reusable HTML partials. Include them in templates or other fragments using the <code>&lt;fragment&gt;</code> tag.</p>
173
191
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- nav.fragment.html --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">fragment</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./"</span>&gt;</span>Home<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./about.html"</span>&gt;</span>About<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /> <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">fragment</span>&gt;</span></code></pre>
174
192
  <p>Include it in a template:</p>
175
193
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- Self-closing: renders empty if fragment file not found --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">fragment</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"nav"</span> /&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- With fallback content if fragment file not found --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">fragment</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"nav"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span>Fallback Nav<span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">fragment</span>&gt;</span></code></pre>
176
194
  <p>Fragments can include other fragments. The maximum nesting depth is controlled by <code>maxFragmentDepth</code> (default: 10).</p>
177
195
 
178
- <h3>File Resolution</h3>
196
+ <h3>Directory Overrides</h3>
179
197
  <p>When a page references a template or a template includes a fragment, the system searches for the file starting in the page's directory and walking up to the root. This means:</p>
180
198
  <ul>
181
199
  <li>Templates and fragments placed at the root apply to all pages</li>
@@ -215,10 +233,13 @@
215
233
  </table>
216
234
  </div>
217
235
 
218
- <h2 id="variables">Variables</h2>
236
+ <h2 id="template-syntax">Template Syntax</h2>
237
+ <p>Use these tags and expressions inside templates, fragments, and pages to produce dynamic output.</p>
238
+
239
+ <h3 id="variables">Variables</h3>
219
240
  <p>Use <code>&#123;&#123;variableName&#125;&#125;</code> syntax to insert dynamic values into templates and fragments.</p>
220
241
 
221
- <h3>Built-in Variables</h3>
242
+ <h4>Built-in Variables</h4>
222
243
  <div class="table-wrapper mb">
223
244
  <table>
224
245
  <thead>
@@ -239,24 +260,24 @@
239
260
  </table>
240
261
  </div>
241
262
 
242
- <h3>Page Attributes as Variables</h3>
263
+ <h4>Page Attributes as Variables</h4>
243
264
  <p>Any attribute on the <code>&lt;page&gt;</code> tag is available as a variable in the template:</p>
244
265
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- page file --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">page</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"My Page"</span> <span class="hljs-attr">author</span>=<span class="hljs-string">"Dustin"</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">page</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- template file --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>&#123;&#123;title&#125;&#125;<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"author"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"&#123;&#123;author&#125;&#125;"</span> /&gt;</span></code></pre>
245
266
 
246
- <h3>Globals and State</h3>
267
+ <h4>Globals and State</h4>
247
268
  <p>Additional variables can be provided through the <code>globals</code> and <code>state</code> configuration objects. Globals and state are merged with page attributes, with page attributes taking priority.</p>
248
269
  <pre><code class="hljs javascript"><span class="hljs-comment">// .config.js</span><br /><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {<br /> <span class="hljs-attr">templating</span>: {<br /> <span class="hljs-attr">globals</span>: {<br /> <span class="hljs-attr">siteName</span>: <span class="hljs-string">'My Site'</span>,<br /> <span class="hljs-attr">copyright</span>: <span class="hljs-string">'© 2026 My Company'</span><br /> },<br /> <span class="hljs-attr">state</span>: {<br /> <span class="hljs-attr">buildId</span>: () =&gt; <span class="hljs-built_in">Date</span>.now().toString(<span class="hljs-number">36</span>)<br /> }<br /> }<br />};</code></pre>
249
270
  <p>Function values in globals or state are called at render time, allowing dynamic values.</p>
250
271
 
251
- <h3>Dot-Path Access</h3>
272
+ <h4>Dot-Path Access</h4>
252
273
  <p>Variables support dot notation for nested object access:</p>
253
274
  <pre><code class="hljs xml">&#123;&#123;site.name&#125;&#125;<br />&#123;&#123;author.email&#125;&#125;</code></pre>
254
275
 
255
- <h2 id="conditionals">Conditionals</h2>
276
+ <h3 id="conditionals">Conditionals</h3>
256
277
  <p>Use <code>&lt;if&gt;</code> blocks to conditionally include content based on variable values.</p>
257
278
  <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"showBanner"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"banner"</span>&gt;</span>Welcome!<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span></code></pre>
258
279
 
259
- <h3>Supported Operators</h3>
280
+ <h4>Supported Operators</h4>
260
281
  <p>Conditions support a full expression syntax:</p>
261
282
  <div class="table-wrapper mb">
262
283
  <table>
@@ -278,10 +299,10 @@
278
299
  </table>
279
300
  </div>
280
301
 
281
- <h3>Examples</h3>
302
+ <h4>Examples</h4>
282
303
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- Truthy check --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"isLoggedIn"</span>&gt;</span>Welcome back!<span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- Negation --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"!isLoggedIn"</span>&gt;</span>Please log in.<span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- String comparison --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"env === 'production'"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"analytics.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- Compound conditions --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"isAdmin &amp;&amp; hasPermission"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/admin"</span>&gt;</span>Admin Panel<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span></code></pre>
283
304
 
284
- <h2 id="loops">Loops</h2>
305
+ <h3 id="loops">Loops</h3>
285
306
  <p>Use <code>&lt;foreach&gt;</code> blocks to iterate over arrays.</p>
286
307
  <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">foreach</span> <span class="hljs-attr">in</span>=<span class="hljs-string">"navLinks"</span> <span class="hljs-attr">as</span>=<span class="hljs-string">"link"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"&#123;&#123;link.url&#125;&#125;"</span>&gt;</span>&#123;&#123;link.label&#125;&#125;<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">foreach</span>&gt;</span></code></pre>
287
308
  <p>The <code>in</code> attribute references an array variable and <code>as</code> names the loop variable. The loop variable supports dot-path access for object items.</p>
@@ -342,8 +363,10 @@
342
363
  <div style="height:25vh"></div>
343
364
  <script
344
365
  type="module"
345
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
366
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
346
367
  ></script>
347
368
 
369
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Accordion.js" type="module"></script>
370
+
348
371
  </body>
349
372
  </html>
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -29,7 +29,7 @@
29
29
  <div style="height:25vh"></div>
30
30
  <script
31
31
  type="module"
32
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
32
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
33
33
  ></script>
34
34
  <location name="scripts"></location>
35
35
  </body>
@@ -35,11 +35,11 @@
35
35
  <location name="links" />
36
36
  </menu>
37
37
  </k-aside>
38
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
39
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
40
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
41
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
42
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
38
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
39
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
40
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
41
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
42
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
43
43
  <script>
44
44
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
45
45
  await window.customElements.whenDefined('k-aside');
@@ -2,28 +2,33 @@
2
2
  <content>
3
3
  <p>Kempo Server includes a built-in XML-based templating system for building static sites with shared layouts, reusable fragments, and dynamic content. Pages can be pre-rendered at build time or server-side rendered on each request.</p>
4
4
 
5
- <nav class="b r mb p">
6
- <h4 class="mt0">On This Page</h4>
7
- <ul>
8
- <li><a href="#overview">Overview</a></li>
9
- <li><a href="#file-types">File Types</a></li>
10
- <li><a href="#templates">Templates</a></li>
11
- <li><a href="#pages">Pages</a>
12
- <ul>
13
- <li><a href="#frontmatter">Frontmatter</a></li>
14
- </ul>
15
- </li>
16
- <li><a href="#fragments">Fragments</a></li>
17
- <li><a href="#global-files">Global Files</a></li>
18
- <li><a href="#fragments-vs-globals">Fragments vs. Global Files</a></li>
19
- <li><a href="#variables">Variables</a></li>
20
- <li><a href="#conditionals">Conditionals</a></li>
21
- <li><a href="#loops">Loops</a></li>
22
- <li><a href="#rendering">Rendering</a></li>
23
- <li><a href="#ssr">Server-Side Rendering</a></li>
24
- <li><a href="#configuration">Configuration</a></li>
25
- </ul>
26
- </nav>
5
+ <k-accordion persistent-id="templating-toc" class="mb">
6
+ <k-accordion-header for-panel="toc">Table of Contents</k-accordion-header>
7
+ <k-accordion-panel name="toc">
8
+ <ul class="m">
9
+ <li><a href="#overview">Overview</a></li>
10
+ <li><a href="#templates">Templates</a></li>
11
+ <li><a href="#pages">Pages</a>
12
+ <ul>
13
+ <li><a href="#frontmatter">Frontmatter</a></li>
14
+ </ul>
15
+ </li>
16
+ <li><a href="#fragments">Fragments</a></li>
17
+ <li><a href="#global-files">Global Files</a></li>
18
+ <li><a href="#fragments-vs-globals">Fragments vs. Global Files</a></li>
19
+ <li><a href="#template-syntax">Template Syntax</a>
20
+ <ul>
21
+ <li><a href="#variables">Variables</a></li>
22
+ <li><a href="#conditionals">Conditionals</a></li>
23
+ <li><a href="#loops">Loops</a></li>
24
+ </ul>
25
+ </li>
26
+ <li><a href="#rendering">Rendering</a></li>
27
+ <li><a href="#ssr">Server-Side Rendering</a></li>
28
+ <li><a href="#configuration">Configuration</a></li>
29
+ </ul>
30
+ </k-accordion-panel>
31
+ </k-accordion>
27
32
 
28
33
  <h2 id="overview">Overview</h2>
29
34
  <p>The templating system uses four file types that work together:</p>
@@ -33,12 +38,23 @@
33
38
  <li><strong>Fragments</strong> (<code>*.fragment.html</code>) &mdash; Reusable HTML partials included in templates or other fragments</li>
34
39
  <li><strong>Globals</strong> (<code>*.global.html</code>) &mdash; Site-wide content blocks automatically injected into every page render</li>
35
40
  </ul>
36
- <p>All three file types are blocked from being served directly by the default <code>disallowedRegex</code> configuration.</p>
37
-
38
- <h2 id="file-types">File Types</h2>
39
41
  <p>A typical project structure:</p>
40
42
  <pre><code class="hljs markdown">my-site/<br />├─ default.template.html # Shared layout<br />├─ nav.fragment.html # Reusable navigation<br />├─ footer.fragment.html # Reusable footer<br />├─ analytics.global.html # Site-wide scripts injected into every page<br />├─ index.page.html # Homepage → renders to index.html<br />├─ about.page.html # About → renders to about.html<br />├─ blog/<br />│ ├─ index.page.html # Blog index → blog/index.html<br />│ ├─ post-1.page.html # Blog post → blog/post-1.html<br /></code></pre>
41
- <p>Templates and fragments are resolved by walking up the directory tree from the page file to the root, so subdirectories can override them by providing their own versions.</p>
43
+ <p>All four file types are blocked from being served directly by the default <code>disallowedRegex</code> configuration. Each type is located differently at startup:</p>
44
+ <div class="table-wrapper mb">
45
+ <table>
46
+ <thead>
47
+ <tr><th>File Type</th><th>How It Is Found</th></tr>
48
+ </thead>
49
+ <tbody>
50
+ <tr><td><code>*.page.html</code></td><td>Recursive scan of the entire root directory tree &mdash; pages in any subdirectory are discovered automatically.</td></tr>
51
+ <tr><td><code>*.global.html</code></td><td>Recursive scan of the entire root directory tree &mdash; globals in any subdirectory are merged together automatically.</td></tr>
52
+ <tr><td><code>*.template.html</code></td><td>Walks <strong>up</strong> the directory tree from the page file&rsquo;s directory toward the root. The nearest match wins. A subdirectory template overrides the root one for pages below it.</td></tr>
53
+ <tr><td><code>*.fragment.html</code></td><td>Same up-walk as templates &mdash; starts at the including file&rsquo;s directory and searches each ancestor up to the root. Nearest match wins.</td></tr>
54
+ </tbody>
55
+ </table>
56
+ </div>
57
+ <p>Because pages and globals use a downward recursive scan, you can freely organize them into subdirectories and they will still be found. Templates and fragments use an upward walk, so they must live in the same directory as the files that reference them, or in a parent directory. Placing a template or fragment in a sibling or cousin directory will not be found.</p>
42
58
 
43
59
  <h2 id="templates">Templates</h2>
44
60
  <p>A template defines the shared HTML structure for your pages. Use <code>&lt;location&gt;</code> tags to define named content slots that pages will fill.</p>
@@ -69,13 +85,15 @@
69
85
  description: Overview of the routing system
70
86
  --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">page</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Routing"</span>&gt;</span><br /> ...<br /><span class="hljs-tag">&lt;/<span class="hljs-name">page</span>&gt;</span></code></pre>
71
87
  <p>The comment is purely for the author &mdash; nothing before <code>&lt;page&gt;</code> is parsed or rendered.</p>
88
+
89
+ <h2 id="fragments">Fragments</h2>
72
90
  <p>Fragments are reusable HTML partials. Include them in templates or other fragments using the <code>&lt;fragment&gt;</code> tag.</p>
73
91
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- nav.fragment.html --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">fragment</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./"</span>&gt;</span>Home<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./about.html"</span>&gt;</span>About<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /> <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">fragment</span>&gt;</span></code></pre>
74
92
  <p>Include it in a template:</p>
75
93
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- Self-closing: renders empty if fragment file not found --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">fragment</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"nav"</span> /&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- With fallback content if fragment file not found --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">fragment</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"nav"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span>Fallback Nav<span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">fragment</span>&gt;</span></code></pre>
76
94
  <p>Fragments can include other fragments. The maximum nesting depth is controlled by <code>maxFragmentDepth</code> (default: 10).</p>
77
95
 
78
- <h3>File Resolution</h3>
96
+ <h3>Directory Overrides</h3>
79
97
  <p>When a page references a template or a template includes a fragment, the system searches for the file starting in the page's directory and walking up to the root. This means:</p>
80
98
  <ul>
81
99
  <li>Templates and fragments placed at the root apply to all pages</li>
@@ -115,10 +133,13 @@
115
133
  </table>
116
134
  </div>
117
135
 
118
- <h2 id="variables">Variables</h2>
136
+ <h2 id="template-syntax">Template Syntax</h2>
137
+ <p>Use these tags and expressions inside templates, fragments, and pages to produce dynamic output.</p>
138
+
139
+ <h3 id="variables">Variables</h3>
119
140
  <p>Use <code>&#123;&#123;variableName&#125;&#125;</code> syntax to insert dynamic values into templates and fragments.</p>
120
141
 
121
- <h3>Built-in Variables</h3>
142
+ <h4>Built-in Variables</h4>
122
143
  <div class="table-wrapper mb">
123
144
  <table>
124
145
  <thead>
@@ -139,24 +160,24 @@
139
160
  </table>
140
161
  </div>
141
162
 
142
- <h3>Page Attributes as Variables</h3>
163
+ <h4>Page Attributes as Variables</h4>
143
164
  <p>Any attribute on the <code>&lt;page&gt;</code> tag is available as a variable in the template:</p>
144
165
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- page file --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">page</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"My Page"</span> <span class="hljs-attr">author</span>=<span class="hljs-string">"Dustin"</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">page</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- template file --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>&#123;&#123;title&#125;&#125;<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"author"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"&#123;&#123;author&#125;&#125;"</span> /&gt;</span></code></pre>
145
166
 
146
- <h3>Globals and State</h3>
167
+ <h4>Globals and State</h4>
147
168
  <p>Additional variables can be provided through the <code>globals</code> and <code>state</code> configuration objects. Globals and state are merged with page attributes, with page attributes taking priority.</p>
148
169
  <pre><code class="hljs javascript"><span class="hljs-comment">// .config.js</span><br /><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {<br /> <span class="hljs-attr">templating</span>: {<br /> <span class="hljs-attr">globals</span>: {<br /> <span class="hljs-attr">siteName</span>: <span class="hljs-string">'My Site'</span>,<br /> <span class="hljs-attr">copyright</span>: <span class="hljs-string">'© 2026 My Company'</span><br /> },<br /> <span class="hljs-attr">state</span>: {<br /> <span class="hljs-attr">buildId</span>: () =&gt; <span class="hljs-built_in">Date</span>.now().toString(<span class="hljs-number">36</span>)<br /> }<br /> }<br />};</code></pre>
149
170
  <p>Function values in globals or state are called at render time, allowing dynamic values.</p>
150
171
 
151
- <h3>Dot-Path Access</h3>
172
+ <h4>Dot-Path Access</h4>
152
173
  <p>Variables support dot notation for nested object access:</p>
153
174
  <pre><code class="hljs xml">&#123;&#123;site.name&#125;&#125;<br />&#123;&#123;author.email&#125;&#125;</code></pre>
154
175
 
155
- <h2 id="conditionals">Conditionals</h2>
176
+ <h3 id="conditionals">Conditionals</h3>
156
177
  <p>Use <code>&lt;if&gt;</code> blocks to conditionally include content based on variable values.</p>
157
178
  <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"showBanner"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"banner"</span>&gt;</span>Welcome!<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span></code></pre>
158
179
 
159
- <h3>Supported Operators</h3>
180
+ <h4>Supported Operators</h4>
160
181
  <p>Conditions support a full expression syntax:</p>
161
182
  <div class="table-wrapper mb">
162
183
  <table>
@@ -178,10 +199,10 @@
178
199
  </table>
179
200
  </div>
180
201
 
181
- <h3>Examples</h3>
202
+ <h4>Examples</h4>
182
203
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- Truthy check --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"isLoggedIn"</span>&gt;</span>Welcome back!<span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- Negation --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"!isLoggedIn"</span>&gt;</span>Please log in.<span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- String comparison --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"env === 'production'"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"analytics.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- Compound conditions --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"isAdmin &amp;&amp; hasPermission"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/admin"</span>&gt;</span>Admin Panel<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span></code></pre>
183
204
 
184
- <h2 id="loops">Loops</h2>
205
+ <h3 id="loops">Loops</h3>
185
206
  <p>Use <code>&lt;foreach&gt;</code> blocks to iterate over arrays.</p>
186
207
  <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">foreach</span> <span class="hljs-attr">in</span>=<span class="hljs-string">"navLinks"</span> <span class="hljs-attr">as</span>=<span class="hljs-string">"link"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"&#123;&#123;link.url&#125;&#125;"</span>&gt;</span>&#123;&#123;link.label&#125;&#125;<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">foreach</span>&gt;</span></code></pre>
187
208
  <p>The <code>in</code> attribute references an array variable and <code>as</code> names the loop variable. The loop variable supports dot-path access for object items.</p>
@@ -238,4 +259,7 @@
238
259
  <pre><code class="hljs javascript"><span class="hljs-comment">// dev.config.js — live rendering, no rebuild needed</span><br /><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {<br /> <span class="hljs-attr">templating</span>: { <span class="hljs-attr">ssr</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">ssrPriority</span>: <span class="hljs-literal">true</span> }<br />};<br /><br /><span class="hljs-comment">// prod.config.js — static files only</span><br /><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {<br /> <span class="hljs-attr">templating</span>: { <span class="hljs-attr">ssr</span>: <span class="hljs-literal">false</span> }<br />};</code></pre>
239
260
  <pre><code class="hljs bash"><span class="hljs-comment"># Development</span><br />node dist/index.js -r ./docs/src -c dev.config.js<br /><br /><span class="hljs-comment"># Production (serve pre-rendered output)</span><br />node dist/index.js -r ./docs/dist</code></pre>
240
261
  </content>
262
+ <content location="scripts">
263
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Accordion.js" type="module"></script>
264
+ </content>
241
265
  </page>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kempo-server",
3
3
  "type": "module",
4
- "version": "3.0.4",
4
+ "version": "3.0.6",
5
5
  "description": "A lightweight, zero-dependency, file based routing server.",
6
6
  "exports": {
7
7
  "./rescan": "./dist/rescan.js",
package/src/router.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  loggingMiddleware
18
18
  } from './builtinMiddleware.js';
19
19
  import { onRescan } from './rescan.js';
20
- import { renderDir } from './templating/index.js';
20
+ import { renderDir, renderPage } from './templating/index.js';
21
21
 
22
22
  export default async (flags, log) => {
23
23
  log('Initializing router', 3);
@@ -148,6 +148,19 @@ export default async (flags, log) => {
148
148
  const {globals, state, maxFragmentDepth} = config.templating;
149
149
  const count = await renderDir(rootPath, rootPath, globals, state, maxFragmentDepth);
150
150
  log(`Pre-rendered ${count} page(s)`, 2);
151
+
152
+ for(const [urlPattern, dirPath] of Object.entries(config.customRoutes || {})) {
153
+ const baseDirRaw = dirPath.includes('*') ? dirPath.split('*')[0] : dirPath;
154
+ const resolvedBaseDir = (path.isAbsolute(baseDirRaw)
155
+ ? baseDirRaw
156
+ : path.resolve(rootPath, baseDirRaw)).replace(/[/\\]+$/, '');
157
+ try {
158
+ const s = await stat(resolvedBaseDir);
159
+ if(!s.isDirectory()) continue;
160
+ const extraCount = await renderDir(resolvedBaseDir, resolvedBaseDir, globals, state, maxFragmentDepth);
161
+ log(`Pre-rendered ${extraCount} page(s) from custom route: ${urlPattern}`, 2);
162
+ } catch { /* directory doesn't exist or isn't accessible, skip */ }
163
+ }
151
164
  }
152
165
 
153
166
  let files = await getFiles(rootPath, config, log);
@@ -421,33 +434,58 @@ export default async (flags, log) => {
421
434
 
422
435
  // Resolves a custom route path supporting files, directories, and [param] segments.
423
436
  // Returns true if handled, null if path not found.
424
- const serveCustomRoutePath = async (resolvedFilePath, req, res) => {
437
+ // customRootDir: root used for template/fragment lookup when attempting SSR.
438
+ const serveCustomRoutePath = async (resolvedFilePath, req, res, customRootDir = null) => {
425
439
  let fileStat;
426
440
  try {
427
441
  fileStat = await stat(resolvedFilePath);
428
- return await serveResolvedPath(resolvedFilePath, fileStat, {}, req, res);
442
+ const result = await serveResolvedPath(resolvedFilePath, fileStat, {}, req, res);
443
+ if(result) return result;
444
+ // Directory existed but no index/route candidates — fall through to SSR
429
445
  } catch(e) {
430
446
  if(e.code !== 'ENOENT') throw e;
431
447
  }
432
448
 
433
- // Path doesn't exist literally — walk backwards to find the nearest existing
434
- // ancestor directory, then traverse forward with [param] support.
435
- let current = resolvedFilePath;
436
- const remaining = [];
437
- while(current !== path.dirname(current)) {
438
- remaining.unshift(path.basename(current));
439
- current = path.dirname(current);
440
- try {
441
- const s = await stat(current);
442
- if(!s.isDirectory()) break;
443
- const result = await walkDynamic(current, remaining);
444
- if(!result) return null;
445
- const resolvedStat = await stat(result.filePath);
446
- return await serveResolvedPath(result.filePath, resolvedStat, result.params, req, res);
447
- } catch(e2) {
448
- if(e2.code !== 'ENOENT') throw e2;
449
+ if(!fileStat) {
450
+ // Path doesn't exist literally walk backwards to find the nearest existing
451
+ // ancestor directory, then traverse forward with [param] support.
452
+ let current = resolvedFilePath;
453
+ const remaining = [];
454
+ while(current !== path.dirname(current)) {
455
+ remaining.unshift(path.basename(current));
456
+ current = path.dirname(current);
457
+ try {
458
+ const s = await stat(current);
459
+ if(!s.isDirectory()) break;
460
+ const result = await walkDynamic(current, remaining);
461
+ if(!result) break;
462
+ const resolvedStat = await stat(result.filePath);
463
+ const served = await serveResolvedPath(result.filePath, resolvedStat, result.params, req, res);
464
+ if(served) return served;
465
+ break;
466
+ } catch(e2) {
467
+ if(e2.code !== 'ENOENT') throw e2;
468
+ }
449
469
  }
450
470
  }
471
+
472
+ if(config.templating?.ssr && customRootDir) {
473
+ const {globals, state, maxFragmentDepth} = config.templating;
474
+ const base = resolvedFilePath.replace(/\.html$/, '').replace(/[\/\\]+$/, '');
475
+ for(const pageFile of [base + '.page.html', path.join(base, 'index.page.html')]) {
476
+ try {
477
+ await stat(pageFile);
478
+ const html = await renderPage(pageFile, customRootDir, globals, state, maxFragmentDepth);
479
+ res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
480
+ res.end(html);
481
+ log(`SSR rendered custom route: ${pageFile}`, 2);
482
+ return true;
483
+ } catch(e) {
484
+ log(`SSR custom route miss for ${pageFile}: ${e.message}`, 3);
485
+ }
486
+ }
487
+ }
488
+
451
489
  return null;
452
490
  };
453
491
 
@@ -572,7 +610,7 @@ export default async (flags, log) => {
572
610
  const customFilePath = customRoutes.get(matchedKey);
573
611
  log(`Serving custom route: ${normalizedRequestPath} -> ${customFilePath}`, 3);
574
612
  try {
575
- const result = await serveCustomRoutePath(customFilePath, req, res);
613
+ const result = await serveCustomRoutePath(customFilePath, req, res, customFilePath);
576
614
  if(result) return;
577
615
  log(`Custom route path not found: ${customFilePath}`, 1);
578
616
  res.writeHead(404, { 'Content-Type': 'text/plain' });
@@ -592,7 +630,8 @@ export default async (flags, log) => {
592
630
  const resolvedFilePath = resolveWildcardPath(wildcardMatch.filePath, wildcardMatch.matches);
593
631
  log(`Serving wildcard route: ${requestPath} -> ${resolvedFilePath}`, 3);
594
632
  try {
595
- const result = await serveCustomRoutePath(resolvedFilePath, req, res);
633
+ const customRootDir = wildcardMatch.filePath.split('*')[0].replace(/[\/\\]+$/, '');
634
+ const result = await serveCustomRoutePath(resolvedFilePath, req, res, customRootDir);
596
635
  if(result) return;
597
636
  log(`Wildcard route path not found: ${requestPath}`, 2);
598
637
  } catch(error) {