kempo-server 3.0.3 → 3.0.5
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/dist/router.js +1 -1
- package/dist/templating/index.js +1 -1
- package/dist/templating/parse.js +1 -1
- package/docs/caching.html +18 -13
- package/docs/cli-utils.html +16 -13
- package/docs/configuration.html +18 -13
- package/docs/examples.html +16 -13
- package/docs/fs-utils.html +16 -13
- package/docs/getting-started.html +16 -13
- package/docs/index.html +16 -13
- package/docs/middleware.html +16 -13
- package/docs/request-response.html +18 -13
- package/docs/routing.html +16 -13
- package/docs/templating.html +87 -31
- package/docs-src/advanced-links.global.html +10 -0
- package/docs-src/caching.page.html +2 -0
- package/docs-src/configuration.page.html +2 -0
- package/docs-src/getting-started-links.global.html +7 -0
- package/docs-src/index.page.html +3 -0
- package/docs-src/nav.fragment.html +1 -13
- package/docs-src/request-response.page.html +2 -0
- package/docs-src/templating.page.html +71 -18
- package/package.json +2 -2
- package/src/router.js +60 -21
- package/src/templating/index.js +37 -4
- package/src/templating/parse.js +28 -5
- package/tests/router-custom-route-ssr.node-test.js +189 -0
- package/tests/templating-parse.node-test.js +43 -9
- package/tests/templating-render.node-test.js +91 -0
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
<li><a href="#overview">Overview</a></li>
|
|
9
9
|
<li><a href="#file-types">File Types</a></li>
|
|
10
10
|
<li><a href="#templates">Templates</a></li>
|
|
11
|
-
<li><a href="#pages">Pages</a
|
|
11
|
+
<li><a href="#pages">Pages</a>
|
|
12
|
+
<ul>
|
|
13
|
+
<li><a href="#frontmatter">Frontmatter</a></li>
|
|
14
|
+
</ul>
|
|
15
|
+
</li>
|
|
12
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>
|
|
13
19
|
<li><a href="#variables">Variables</a></li>
|
|
14
20
|
<li><a href="#conditionals">Conditionals</a></li>
|
|
15
21
|
<li><a href="#loops">Loops</a></li>
|
|
@@ -20,22 +26,23 @@
|
|
|
20
26
|
</nav>
|
|
21
27
|
|
|
22
28
|
<h2 id="overview">Overview</h2>
|
|
23
|
-
<p>The templating system uses
|
|
29
|
+
<p>The templating system uses four file types that work together:</p>
|
|
24
30
|
<ul>
|
|
25
31
|
<li><strong>Templates</strong> (<code>*.template.html</code>) — Shared page layouts with named content slots</li>
|
|
26
32
|
<li><strong>Pages</strong> (<code>*.page.html</code>) — Individual pages that fill template slots with content</li>
|
|
27
33
|
<li><strong>Fragments</strong> (<code>*.fragment.html</code>) — Reusable HTML partials included in templates or other fragments</li>
|
|
34
|
+
<li><strong>Globals</strong> (<code>*.global.html</code>) — Site-wide content blocks automatically injected into every page render</li>
|
|
28
35
|
</ul>
|
|
29
36
|
<p>All three file types are blocked from being served directly by the default <code>disallowedRegex</code> configuration.</p>
|
|
30
37
|
|
|
31
38
|
<h2 id="file-types">File Types</h2>
|
|
32
39
|
<p>A typical project structure:</p>
|
|
33
|
-
<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 />├─ 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>
|
|
40
|
+
<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>
|
|
34
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>
|
|
35
42
|
|
|
36
43
|
<h2 id="templates">Templates</h2>
|
|
37
44
|
<p>A template defines the shared HTML structure for your pages. Use <code><location></code> tags to define named content slots that pages will fill.</p>
|
|
38
|
-
<pre><code class="hljs xml"><span class="hljs-comment"><!-- default.template.html --></span><br /><span class="hljs-meta"><!DOCTYPE html></span><br /><span class="hljs-tag"><<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>></span><br /><span class="hljs-tag"><<span class="hljs-name">head</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span> /></span><br /> <span class="hljs-tag"><<span class="hljs-name">title</span>></span
|
|
45
|
+
<pre><code class="hljs xml"><span class="hljs-comment"><!-- default.template.html --></span><br /><span class="hljs-meta"><!DOCTYPE html></span><br /><span class="hljs-tag"><<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>></span><br /><span class="hljs-tag"><<span class="hljs-name">head</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span> /></span><br /> <span class="hljs-tag"><<span class="hljs-name">title</span>></span>{{title}}<span class="hljs-tag"></<span class="hljs-name">title</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">head</span>></span><br /><span class="hljs-tag"><<span class="hljs-name">body</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">fragment</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"nav"</span> /></span><br /> <span class="hljs-tag"><<span class="hljs-name">main</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">h1</span>></span>{{pageName}}<span class="hljs-tag"></<span class="hljs-name">h1</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">location</span> /></span><br /> <span class="hljs-tag"></<span class="hljs-name">main</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">location</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"scripts"</span>></span><br /> <span class="hljs-comment"><!-- default scripts if page doesn't provide any --></span><br /> <span class="hljs-tag"></<span class="hljs-name">location</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">body</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">html</span>></span></code></pre>
|
|
39
46
|
|
|
40
47
|
<h3>Location Tags</h3>
|
|
41
48
|
<p>Locations are named slots in a template that pages fill with content. The <code>name</code> attribute is optional — a <code><location></code> without a name defaults to <code>"default"</code>.</p>
|
|
@@ -46,15 +53,22 @@
|
|
|
46
53
|
</ul>
|
|
47
54
|
|
|
48
55
|
<h2 id="pages">Pages</h2>
|
|
49
|
-
<p>A page file wraps its content in a <code><page></code> root element and uses <code><content></code> blocks to fill the template's locations. The <code>location</code> attribute is optional — a <code><content></code> block without one targets the <code>"default"</code> location. Multiple <code><content></code> blocks targeting the same location are
|
|
56
|
+
<p>A page file wraps its content in a <code><page></code> root element and uses <code><content></code> blocks to fill the template's locations. The <code>location</code> attribute is optional — a <code><content></code> block without one targets the <code>"default"</code> location. Multiple <code><content></code> blocks targeting the same location are merged and sorted by <code>priority</code> (higher first, default <code>0</code>).</p>
|
|
50
57
|
<pre><code class="hljs xml"><span class="hljs-comment"><!-- about.page.html → renders to about.html --></span><br /><span class="hljs-tag"><<span class="hljs-name">page</span> <span class="hljs-attr">pageName</span>=<span class="hljs-string">"About Us"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"About - My Site"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">content</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">p</span>></span>Welcome to our about page.<span class="hljs-tag"></<span class="hljs-name">p</span>></span><br /> <span class="hljs-tag"></<span class="hljs-name">content</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">page</span>></span></code></pre>
|
|
51
58
|
|
|
52
59
|
<h3>Page Attributes</h3>
|
|
53
|
-
<p>Attributes on the <code><page></code> tag become template variables. In the example above, <code
|
|
60
|
+
<p>Attributes on the <code><page></code> tag become template variables. In the example above, <code>{{pageName}}</code> resolves to <code>About Us</code> and <code>{{title}}</code> resolves to <code>About - My Site</code>.</p>
|
|
54
61
|
<p>The special <code>template</code> attribute selects which template to use. It defaults to <code>default</code>, which looks for <code>default.template.html</code>.</p>
|
|
55
62
|
<pre><code class="hljs xml"><span class="hljs-comment"><!-- Uses blog.template.html instead of default.template.html --></span><br /><span class="hljs-tag"><<span class="hljs-name">page</span> <span class="hljs-attr">template</span>=<span class="hljs-string">"blog"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"My Post"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">content</span> <span class="hljs-attr">location</span>=<span class="hljs-string">"main"</span>></span>...<span class="hljs-tag"></<span class="hljs-name">content</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">page</span>></span></code></pre>
|
|
56
63
|
|
|
57
|
-
<
|
|
64
|
+
<h3 id="frontmatter">Frontmatter</h3>
|
|
65
|
+
<p>Any content before the opening <code><page></code> tag is ignored by the renderer. This makes it a natural place for author metadata, description, dates, or any notes that belong with the file but shouldn't appear in the output — similar to frontmatter in other static site generators.</p>
|
|
66
|
+
<pre><code class="hljs xml"><span class="hljs-comment"><!--
|
|
67
|
+
author: Dustin Poissant
|
|
68
|
+
date: 2026-04-10
|
|
69
|
+
description: Overview of the routing system
|
|
70
|
+
--></span><br /><span class="hljs-tag"><<span class="hljs-name">page</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Routing"</span>></span><br /> ...<br /><span class="hljs-tag"></<span class="hljs-name">page</span>></span></code></pre>
|
|
71
|
+
<p>The comment is purely for the author — nothing before <code><page></code> is parsed or rendered.</p>
|
|
58
72
|
<p>Fragments are reusable HTML partials. Include them in templates or other fragments using the <code><fragment></code> tag.</p>
|
|
59
73
|
<pre><code class="hljs xml"><span class="hljs-comment"><!-- nav.fragment.html --></span><br /><span class="hljs-tag"><<span class="hljs-name">fragment</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">nav</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./"</span>></span>Home<span class="hljs-tag"></<span class="hljs-name">a</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./about.html"</span>></span>About<span class="hljs-tag"></<span class="hljs-name">a</span>></span><br /> <span class="hljs-tag"></<span class="hljs-name">nav</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">fragment</span>></span></code></pre>
|
|
60
74
|
<p>Include it in a template:</p>
|
|
@@ -68,10 +82,44 @@
|
|
|
68
82
|
<li>A subdirectory can provide its own version to override the parent</li>
|
|
69
83
|
</ul>
|
|
70
84
|
|
|
85
|
+
<h2 id="global-files">Global Files</h2>
|
|
86
|
+
<p>Any file named <code>*.global.html</code> anywhere under the root directory is automatically loaded and its <code><content></code> blocks are injected into every page render. This is the right place to put site-wide scripts, meta tags, nav links, or anything else that should appear across all pages without touching every template or page file.</p>
|
|
87
|
+
<pre><code class="hljs xml"><span class="hljs-comment"><!-- analytics.global.html --></span><br /><span class="hljs-tag"><<span class="hljs-name">content</span> <span class="hljs-attr">location</span>=<span class="hljs-string">"scripts"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"analytics.js"</span> <span class="hljs-attr">defer</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">content</span>></span></code></pre>
|
|
88
|
+
<p>The template just needs a matching <code><location></code> tag — the global file fills it automatically on every page.</p>
|
|
89
|
+
|
|
90
|
+
<h3>Priority</h3>
|
|
91
|
+
<p>When multiple <code><content></code> blocks — from global files, multiple global files, or the page itself — target the same location, a <code>priority</code> attribute controls their output order. Higher numbers come first. The default is <code>0</code>.</p>
|
|
92
|
+
<pre><code class="hljs xml"><span class="hljs-comment"><!-- framework.global.html --></span><br /><span class="hljs-tag"><<span class="hljs-name">content</span> <span class="hljs-attr">location</span>=<span class="hljs-string">"scripts"</span> <span class="hljs-attr">priority</span>=<span class="hljs-string">"10"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"framework.js"</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">content</span>></span><br /><br /><span class="hljs-comment"><!-- analytics.global.html — priority defaults to 0 --></span><br /><span class="hljs-tag"><<span class="hljs-name">content</span> <span class="hljs-attr">location</span>=<span class="hljs-string">"scripts"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"analytics.js"</span> <span class="hljs-attr">defer</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">content</span>></span></code></pre>
|
|
93
|
+
<p>Output: <code>framework.js</code> first (priority 10), then <code>analytics.js</code> (priority 0). The same <code>priority</code> attribute works on <code><content></code> blocks in page files too, so a page can insert content before or after global content in the same location.</p>
|
|
94
|
+
|
|
95
|
+
<h3>Locations Inside Page Content</h3>
|
|
96
|
+
<p>You can place <code><location></code> tags inside a page's <code><content></code> block. Global content fills those locations before the block is inserted into the template. This lets a page pull in a global snippet at a specific spot without knowing what the snippet contains.</p>
|
|
97
|
+
<pre><code class="hljs xml"><span class="hljs-comment"><!-- nav-links.global.html --></span><br /><span class="hljs-tag"><<span class="hljs-name">content</span> <span class="hljs-attr">location</span>=<span class="hljs-string">"links"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./about.html"</span>></span>About<span class="hljs-tag"></<span class="hljs-name">a</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">content</span>></span><br /><br /><span class="hljs-comment"><!-- nav.fragment.html — location tag pulls in the global links --></span><br /><span class="hljs-tag"><<span class="hljs-name">fragment</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">nav</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">location</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"links"</span> /></span><br /> <span class="hljs-tag"></<span class="hljs-name">nav</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">fragment</span>></span></code></pre>
|
|
98
|
+
|
|
99
|
+
<h2 id="fragments-vs-globals">Fragments vs. Global Files</h2>
|
|
100
|
+
<p>Fragments and global files both let you share HTML across pages, but they work in opposite directions.</p>
|
|
101
|
+
<p><strong>Fragments are a pull.</strong> The template (or another fragment) explicitly asks for one thing by name: <code><fragment name="nav" /></code>. The fragment file is a single reusable block of markup. Only what is asked for gets included, and where it lands is decided entirely by the caller.</p>
|
|
102
|
+
<p><strong>Global files are a push.</strong> A <code>*.global.html</code> file declares content and targets a location, and that content is automatically injected into every page render without the template needing to know it exists. You can have many global files each contributing to the same location, and a page can add its own content to that same location alongside them. The final output is all of those contributions merged and ordered by <code>priority</code>.</p>
|
|
103
|
+
<div class="table-wrapper mb">
|
|
104
|
+
<table>
|
|
105
|
+
<thead>
|
|
106
|
+
<tr><th></th><th>Fragments</th><th>Global Files</th></tr>
|
|
107
|
+
</thead>
|
|
108
|
+
<tbody>
|
|
109
|
+
<tr><td>Direction</td><td>Pull — caller decides what to include</td><td>Push — file injects itself automatically</td></tr>
|
|
110
|
+
<tr><td>How many per location</td><td>One (the named file)</td><td>Many — all globals targeting the same location are merged</td></tr>
|
|
111
|
+
<tr><td>Page can contribute too?</td><td>No — the fragment replaces the tag entirely</td><td>Yes — page <code><content></code> blocks merge with global content</td></tr>
|
|
112
|
+
<tr><td>Requires template change?</td><td>Yes — caller must add <code><fragment name="..." /></code></td><td>No — just add a <code><location></code> and the globals fill it</td></tr>
|
|
113
|
+
<tr><td>Good for</td><td>Reusable structural blocks (nav, footer, sidebar)</td><td>Site-wide injections (scripts, meta tags, nav links)</td></tr>
|
|
114
|
+
</tbody>
|
|
115
|
+
</table>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
71
118
|
<h2 id="variables">Variables</h2>
|
|
72
|
-
<p>Use <code
|
|
119
|
+
<p>Use <code>{{variableName}}</code> syntax to insert dynamic values into templates and fragments.</p>
|
|
73
120
|
|
|
74
121
|
<h3>Built-in Variables</h3>
|
|
122
|
+
<div class="table-wrapper mb">
|
|
75
123
|
<table>
|
|
76
124
|
<thead>
|
|
77
125
|
<tr>
|
|
@@ -80,19 +128,20 @@
|
|
|
80
128
|
</tr>
|
|
81
129
|
</thead>
|
|
82
130
|
<tbody>
|
|
83
|
-
<tr><td><code
|
|
84
|
-
<tr><td><code
|
|
85
|
-
<tr><td><code
|
|
86
|
-
<tr><td><code
|
|
87
|
-
<tr><td><code
|
|
88
|
-
<tr><td><code
|
|
89
|
-
<tr><td><code
|
|
131
|
+
<tr><td><code>{{pathToRoot}}</code></td><td>Relative path from the page to the root directory (e.g. <code>./</code>, <code>../</code>, <code>../../</code>)</td></tr>
|
|
132
|
+
<tr><td><code>{{year}}</code></td><td>Current four-digit year</td></tr>
|
|
133
|
+
<tr><td><code>{{date}}</code></td><td>Current date in ISO format (<code>YYYY-MM-DD</code>)</td></tr>
|
|
134
|
+
<tr><td><code>{{datetime}}</code></td><td>Full ISO 8601 datetime string</td></tr>
|
|
135
|
+
<tr><td><code>{{timestamp}}</code></td><td>Unix timestamp in milliseconds</td></tr>
|
|
136
|
+
<tr><td><code>{{version}}</code></td><td>Version from the root <code>package.json</code></td></tr>
|
|
137
|
+
<tr><td><code>{{env}}</code></td><td>Value of <code>NODE_ENV</code></td></tr>
|
|
90
138
|
</tbody>
|
|
91
139
|
</table>
|
|
140
|
+
</div>
|
|
92
141
|
|
|
93
142
|
<h3>Page Attributes as Variables</h3>
|
|
94
143
|
<p>Any attribute on the <code><page></code> tag is available as a variable in the template:</p>
|
|
95
|
-
<pre><code class="hljs xml"><span class="hljs-comment"><!-- page file --></span><br /><span class="hljs-tag"><<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>></span>...<span class="hljs-tag"></<span class="hljs-name">page</span>></span><br /><br /><span class="hljs-comment"><!-- template file --></span><br /><span class="hljs-tag"><<span class="hljs-name">title</span>></span
|
|
144
|
+
<pre><code class="hljs xml"><span class="hljs-comment"><!-- page file --></span><br /><span class="hljs-tag"><<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>></span>...<span class="hljs-tag"></<span class="hljs-name">page</span>></span><br /><br /><span class="hljs-comment"><!-- template file --></span><br /><span class="hljs-tag"><<span class="hljs-name">title</span>></span>{{title}}<span class="hljs-tag"></<span class="hljs-name">title</span>></span><br /><span class="hljs-tag"><<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">"{{author}}"</span> /></span></code></pre>
|
|
96
145
|
|
|
97
146
|
<h3>Globals and State</h3>
|
|
98
147
|
<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>
|
|
@@ -101,7 +150,7 @@
|
|
|
101
150
|
|
|
102
151
|
<h3>Dot-Path Access</h3>
|
|
103
152
|
<p>Variables support dot notation for nested object access:</p>
|
|
104
|
-
<pre><code class="hljs xml"
|
|
153
|
+
<pre><code class="hljs xml">{{site.name}}<br />{{author.email}}</code></pre>
|
|
105
154
|
|
|
106
155
|
<h2 id="conditionals">Conditionals</h2>
|
|
107
156
|
<p>Use <code><if></code> blocks to conditionally include content based on variable values.</p>
|
|
@@ -109,6 +158,7 @@
|
|
|
109
158
|
|
|
110
159
|
<h3>Supported Operators</h3>
|
|
111
160
|
<p>Conditions support a full expression syntax:</p>
|
|
161
|
+
<div class="table-wrapper mb">
|
|
112
162
|
<table>
|
|
113
163
|
<thead>
|
|
114
164
|
<tr>
|
|
@@ -126,13 +176,14 @@
|
|
|
126
176
|
<tr><td><code>( )</code></td><td>Grouping</td></tr>
|
|
127
177
|
</tbody>
|
|
128
178
|
</table>
|
|
179
|
+
</div>
|
|
129
180
|
|
|
130
181
|
<h3>Examples</h3>
|
|
131
182
|
<pre><code class="hljs xml"><span class="hljs-comment"><!-- Truthy check --></span><br /><span class="hljs-tag"><<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"isLoggedIn"</span>></span>Welcome back!<span class="hljs-tag"></<span class="hljs-name">if</span>></span><br /><br /><span class="hljs-comment"><!-- Negation --></span><br /><span class="hljs-tag"><<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"!isLoggedIn"</span>></span>Please log in.<span class="hljs-tag"></<span class="hljs-name">if</span>></span><br /><br /><span class="hljs-comment"><!-- String comparison --></span><br /><span class="hljs-tag"><<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"env === 'production'"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"analytics.js"</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">if</span>></span><br /><br /><span class="hljs-comment"><!-- Compound conditions --></span><br /><span class="hljs-tag"><<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"isAdmin && hasPermission"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/admin"</span>></span>Admin Panel<span class="hljs-tag"></<span class="hljs-name">a</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">if</span>></span></code></pre>
|
|
132
183
|
|
|
133
184
|
<h2 id="loops">Loops</h2>
|
|
134
185
|
<p>Use <code><foreach></code> blocks to iterate over arrays.</p>
|
|
135
|
-
<pre><code class="hljs xml"><span class="hljs-tag"><<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>></span><br /> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"
|
|
186
|
+
<pre><code class="hljs xml"><span class="hljs-tag"><<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>></span><br /> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"{{link.url}}"</span>></span>{{link.label}}<span class="hljs-tag"></<span class="hljs-name">a</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">foreach</span>></span></code></pre>
|
|
136
187
|
<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>
|
|
137
188
|
|
|
138
189
|
<p>Provide the array through globals or state:</p>
|
|
@@ -162,6 +213,7 @@
|
|
|
162
213
|
|
|
163
214
|
<h2 id="configuration">Configuration</h2>
|
|
164
215
|
<p>All templating options live under the <code>templating</code> key in your configuration file.</p>
|
|
216
|
+
<div class="table-wrapper mb">
|
|
165
217
|
<table>
|
|
166
218
|
<thead>
|
|
167
219
|
<tr>
|
|
@@ -179,6 +231,7 @@
|
|
|
179
231
|
<tr><td><code>maxFragmentDepth</code></td><td><code>10</code></td><td>Maximum fragment nesting depth</td></tr>
|
|
180
232
|
</tbody>
|
|
181
233
|
</table>
|
|
234
|
+
</div>
|
|
182
235
|
|
|
183
236
|
<h3>Development vs Production</h3>
|
|
184
237
|
<p>A common pattern is to use separate config files:</p>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo-server",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.0.
|
|
4
|
+
"version": "3.0.5",
|
|
5
5
|
"description": "A lightweight, zero-dependency, file based routing server.",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./rescan": "./dist/rescan.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "node scripts/build.js",
|
|
18
18
|
"docs": "node dist/index.js -r ./docs",
|
|
19
|
-
"
|
|
19
|
+
"dev": "node dist/index.js -r ./docs-src",
|
|
20
20
|
"spa": "node dist/index.js -r ./spa",
|
|
21
21
|
"test": "npx kempo-test",
|
|
22
22
|
"test:gui": "npx kempo-test --gui",
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
|
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) {
|
package/src/templating/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import {
|
|
4
4
|
extractAttrs,
|
|
5
5
|
extractContentBlocks,
|
|
6
|
+
mergeContentBlocks,
|
|
6
7
|
replaceLocations,
|
|
7
8
|
resolveVars,
|
|
8
9
|
resolveIfs,
|
|
@@ -38,10 +39,33 @@ const loadVersion = rootDir => {
|
|
|
38
39
|
}
|
|
39
40
|
};
|
|
40
41
|
|
|
42
|
+
/*
|
|
43
|
+
Walk Directory for *.global.html Files
|
|
44
|
+
*/
|
|
45
|
+
const walkGlobals = async dir => {
|
|
46
|
+
const entries = await readdir(dir, {withFileTypes: true});
|
|
47
|
+
const results = [];
|
|
48
|
+
for(const entry of entries){
|
|
49
|
+
const full = path.join(dir, entry.name);
|
|
50
|
+
if(entry.isDirectory()){
|
|
51
|
+
results.push(...await walkGlobals(full));
|
|
52
|
+
} else if(entry.name.endsWith('.global.html')){
|
|
53
|
+
results.push(full);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return results;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const loadGlobalContent = async rootDir => {
|
|
60
|
+
const files = await walkGlobals(rootDir);
|
|
61
|
+
const maps = await Promise.all(files.map(async f => extractContentBlocks(await readFile(f, 'utf8'))));
|
|
62
|
+
return mergeContentBlocks(...maps);
|
|
63
|
+
};
|
|
64
|
+
|
|
41
65
|
/*
|
|
42
66
|
Render a Single Page
|
|
43
67
|
*/
|
|
44
|
-
const renderPage = async (pageFilePath, rootDir, globals = {}, state = {}, maxDepth = 10) => {
|
|
68
|
+
const renderPage = async (pageFilePath, rootDir, globals = {}, state = {}, maxDepth = 10, preloadedGlobalContent = null) => {
|
|
45
69
|
const pageContent = await readFile(pageFilePath, 'utf8');
|
|
46
70
|
const pageTagMatch = pageContent.match(/^[\s\S]*?<page\s([^>]*)>/);
|
|
47
71
|
if(!pageTagMatch) throw new Error(`Invalid page file: missing <page> root element in ${pageFilePath}`);
|
|
@@ -49,11 +73,20 @@ const renderPage = async (pageFilePath, rootDir, globals = {}, state = {}, maxDe
|
|
|
49
73
|
const templateName = pageAttrs.template || 'default';
|
|
50
74
|
delete pageAttrs.template;
|
|
51
75
|
|
|
52
|
-
const contentBlocks = extractContentBlocks(pageContent);
|
|
53
76
|
const pageDir = path.dirname(pageFilePath);
|
|
54
77
|
const templateFile = findFileUpSync(`${templateName}.template.html`, pageDir, rootDir);
|
|
55
78
|
if(!templateFile) throw new Error(`Template not found: ${templateName}.template.html (searched from ${pageDir} to ${rootDir})`);
|
|
56
79
|
|
|
80
|
+
const globalContent = preloadedGlobalContent ?? await loadGlobalContent(rootDir);
|
|
81
|
+
const rawPageBlocks = extractContentBlocks(pageContent);
|
|
82
|
+
|
|
83
|
+
// Allow <location> tags inside page content blocks to be filled by global content
|
|
84
|
+
const pageBlocks = {};
|
|
85
|
+
for(const [name, entries] of Object.entries(rawPageBlocks)){
|
|
86
|
+
pageBlocks[name] = entries.map(e => ({...e, html: replaceLocations(e.html, globalContent)}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const contentBlocks = mergeContentBlocks(pageBlocks, globalContent);
|
|
57
90
|
let templateHtml = readFileSync(templateFile, 'utf8');
|
|
58
91
|
|
|
59
92
|
const findFragmentFile = name => {
|
|
@@ -115,14 +148,14 @@ const walkPages = async dir => {
|
|
|
115
148
|
Render All Pages in a Directory
|
|
116
149
|
*/
|
|
117
150
|
const renderDir = async (inputDir, outputDir, globals = {}, state = {}, maxDepth = 10) => {
|
|
118
|
-
const pages = await walkPages(inputDir);
|
|
151
|
+
const [pages, globalContent] = await Promise.all([walkPages(inputDir), loadGlobalContent(inputDir)]);
|
|
119
152
|
let count = 0;
|
|
120
153
|
for(const page of pages){
|
|
121
154
|
const rel = path.relative(inputDir, page);
|
|
122
155
|
const outRel = rel.replace(/\.page\.html$/, '.html');
|
|
123
156
|
const outPath = path.join(outputDir, outRel);
|
|
124
157
|
await mkdir(path.dirname(outPath), {recursive: true});
|
|
125
|
-
const html = await renderPage(page, inputDir, globals, state, maxDepth);
|
|
158
|
+
const html = await renderPage(page, inputDir, globals, state, maxDepth, globalContent);
|
|
126
159
|
await writeFile(outPath, html, 'utf8');
|
|
127
160
|
count++;
|
|
128
161
|
}
|
package/src/templating/parse.js
CHANGED
|
@@ -16,25 +16,47 @@ const extractAttrs = tagString => {
|
|
|
16
16
|
*/
|
|
17
17
|
const extractContentBlocks = xml => {
|
|
18
18
|
const blocks = {};
|
|
19
|
-
const re = /<content(?:\s+
|
|
19
|
+
const re = /<content(?:\s+([^>]*))?\s*>([\s\S]*?)<\/content>/g;
|
|
20
20
|
let match;
|
|
21
21
|
while((match = re.exec(xml)) !== null){
|
|
22
|
-
const
|
|
23
|
-
|
|
22
|
+
const attrs = extractAttrs(match[1] || '');
|
|
23
|
+
const name = attrs.location || 'default';
|
|
24
|
+
const priority = parseInt(attrs.priority || '0', 10);
|
|
25
|
+
if(!blocks[name]) blocks[name] = [];
|
|
26
|
+
blocks[name].push({html: match[2], priority});
|
|
24
27
|
}
|
|
25
28
|
return blocks;
|
|
26
29
|
};
|
|
27
30
|
|
|
31
|
+
/*
|
|
32
|
+
Content Block Merging
|
|
33
|
+
*/
|
|
34
|
+
const mergeContentBlocks = (...maps) => {
|
|
35
|
+
const merged = {};
|
|
36
|
+
for(const map of maps){
|
|
37
|
+
for(const [name, entries] of Object.entries(map)){
|
|
38
|
+
if(!merged[name]) merged[name] = [];
|
|
39
|
+
merged[name].push(...entries);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return merged;
|
|
43
|
+
};
|
|
44
|
+
|
|
28
45
|
/*
|
|
29
46
|
Location Replacement
|
|
30
47
|
*/
|
|
48
|
+
const resolveLocation = (entries) => {
|
|
49
|
+
if(!entries?.length) return null;
|
|
50
|
+
return [...entries].sort((a, b) => b.priority - a.priority).map(e => e.html).join('');
|
|
51
|
+
};
|
|
52
|
+
|
|
31
53
|
const replaceLocations = (html, contentMap) =>
|
|
32
54
|
html
|
|
33
55
|
.replace(/<location(?:\s+name="([^"]*)")?>([\s\S]*?)<\/location>/g, (_, name, fallback) =>
|
|
34
|
-
contentMap[name || 'default'] ?? fallback
|
|
56
|
+
resolveLocation(contentMap[name || 'default']) ?? fallback
|
|
35
57
|
)
|
|
36
58
|
.replace(/<location(?:\s+name="([^"]*)")?\s*\/>/g, (_, name) =>
|
|
37
|
-
contentMap[name || 'default'] ?? ''
|
|
59
|
+
resolveLocation(contentMap[name || 'default']) ?? ''
|
|
38
60
|
);
|
|
39
61
|
|
|
40
62
|
/*
|
|
@@ -274,6 +296,7 @@ const evalCondition = (expression, vars) => {
|
|
|
274
296
|
export {
|
|
275
297
|
extractAttrs,
|
|
276
298
|
extractContentBlocks,
|
|
299
|
+
mergeContentBlocks,
|
|
277
300
|
replaceLocations,
|
|
278
301
|
stripFragmentWrapper,
|
|
279
302
|
resolveVars,
|