create-berna-stencil 1.0.58 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,7 +44,7 @@ Building a website from scratch involves a lot of moving parts: templating engin
44
44
 
45
45
  ## Changelog
46
46
 
47
- ### [1.0.0] - 2025-05-27
47
+ ### [2.0.0] - 2025-05-28
48
48
  * Initial release
49
49
  * Eleventy v3.1.2 support
50
50
  * Base project structure and scaffolding CLI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-berna-stencil",
3
- "version": "1.0.58",
3
+ "version": "2.0.0",
4
4
  "description": "Eleventy boilerplate with per-page SCSS/JS pipeline, esbuild bundling, multi-framework CSS support and a built-in page management CLI",
5
5
  "keywords": [],
6
6
  "author": "Michele Garofalo",
@@ -1,7 +1,7 @@
1
1
  <div class="container my-3">
2
2
  <h1>Welcome to <span class="berna-stencil">Berna-Stencil</span></h1>
3
3
  <div class="slogan">The boilerplate you need, simplified</div>
4
-
4
+
5
5
  <div class="guide-filter">
6
6
  <label>
7
7
  <input type="radio" name="guide-filter" value="welcome" checked />
@@ -19,7 +19,7 @@
19
19
  <input type="radio" name="guide-filter" value="javascript" />
20
20
  <span class="filter-pill">Javascript</span>
21
21
  </label>
22
- <label>
22
+ <label>
23
23
  <input type="radio" name="guide-filter" value="creating-pages" />
24
24
  <span class="filter-pill">Creating Pages</span>
25
25
  </label>
@@ -37,88 +37,50 @@
37
37
  </label>
38
38
  </div>
39
39
 
40
- <div class="tabs-container">
40
+ <div class="tabs-container">
41
+
41
42
  <div id="content-welcome" class="tab-content active">
42
43
  <div class="grid" style="display: flex; gap: 1rem; flex-wrap: wrap;">
43
- <a
44
- href="https://bernastencil.com"
45
- class="card"
46
- target="_blank"
47
- rel="noopener noreferrer"
48
- style="flex: 1; min-width: 250px;"
49
- >
44
+ <a href="https://bernastencil.com" class="card" target="_blank" rel="noopener noreferrer" style="flex: 1; min-width: 250px;">
50
45
  <i class="bi bi-book card-icon" aria-hidden="true"></i>
51
46
  <h3>Documentation</h3>
52
- <p>
53
- Everything you need to get started, from setup to advanced topics and
54
- customizations
55
- </p>
56
- <span class="card-link"
57
- >Go to documentation <span class="bi bi-arrow-right"></span
58
- ></span>
47
+ <p>Everything you need to get started, from setup to advanced topics and customizations</p>
48
+ <span class="card-link">Go to documentation <span class="bi bi-arrow-right"></span></span>
59
49
  </a>
60
-
61
- <a
62
- href="https://github.com/rhaastrake/berna-stencil"
63
- class="card"
64
- target="_blank"
65
- rel="noopener noreferrer"
66
- style="flex: 1; min-width: 250px;"
67
- >
50
+ <a href="https://github.com/rhaastrake/berna-stencil" class="card" target="_blank" rel="noopener noreferrer" style="flex: 1; min-width: 250px;">
68
51
  <i class="bi bi-github card-icon" aria-hidden="true"></i>
69
52
  <h3>Github repository</h3>
70
53
  <p>Community-driven. Contributions, issues and PRs are welcome.</p>
71
- <span class="card-link"
72
- >Open the repository <span class="bi bi-arrow-right"></span
73
- ></span>
54
+ <span class="card-link">Open the repository <span class="bi bi-arrow-right"></span></span>
74
55
  </a>
75
56
  </div>
76
57
  </div>
77
- {# ASSISTANT #}
58
+
78
59
  <div id="content-assistant" class="tab-content">
79
60
  <div class="markdown-body">
80
61
  <h2>Assistant CLI</h2>
81
62
  <p>An interactive CLI to manage pages without touching files manually.</p>
82
-
83
63
  <pre><code>npm run assistant</code></pre>
84
-
85
64
  <h3>Menu</h3>
86
65
  <pre><code>1. Create page
87
66
  2. Remove page
88
67
  3. Rename page
89
68
  4. Configure output path</code></pre>
90
-
91
69
  <p>Use <code>CTRL/CMD + C</code> to exit.</p>
92
-
93
70
  <h3>Create page</h3>
94
71
  <p>Enter a page name in any format — the CLI converts it to kebab-case automatically.</p>
95
72
  <p>For a page named <code>my-page</code>, the following files are created:</p>
96
-
97
73
  <table>
98
74
  <thead>
99
- <tr>
100
- <th>File</th>
101
- <th>Purpose</th>
102
- </tr>
75
+ <tr><th>File</th><th>Purpose</th></tr>
103
76
  </thead>
104
77
  <tbody>
105
- <tr>
106
- <td><code>src/frontend/scss/pages/myPage.scss</code></td>
107
- <td>SCSS entry point</td>
108
- </tr>
109
- <tr>
110
- <td><code>src/frontend/js/pages/myPage.js</code></td>
111
- <td>JS entry point</td>
112
- </tr>
113
- <tr>
114
- <td><code>src/frontend/_routes/my-page.njk</code></td>
115
- <td>Nunjucks template</td>
116
- </tr>
78
+ <tr><td><code>src/frontend/scss/pages/myPage.scss</code></td><td>SCSS entry point</td></tr>
79
+ <tr><td><code>src/frontend/js/pages/myPage.js</code></td><td>JS entry point</td></tr>
80
+ <tr><td><code>src/frontend/_routes/my-page.njk</code></td><td>Nunjucks template</td></tr>
117
81
  </tbody>
118
82
  </table>
119
-
120
83
  <p>It also adds an <code>elif</code> block in <code>includes.njk</code> and a stub entry in <code>site.json</code>:</p>
121
-
122
84
  <pre><code>"myPage": {
123
85
  "seo": {
124
86
  "title": "My Page",
@@ -129,29 +91,23 @@
129
91
  "js": []
130
92
  }
131
93
  }</code></pre>
132
-
133
94
  <h3>Remove page</h3>
134
95
  <p>Deletes all source files for the page and cleans up the output directory, <code>includes.njk</code>, and <code>site.json</code>.</p>
135
-
136
96
  <h3>Rename page</h3>
137
97
  <p>Renames all three source files, updates the <code>elif</code> block in <code>includes.njk</code>, and renames the record in <code>site.json</code> while preserving all existing fields.</p>
138
-
139
98
  <h3>Configure output path</h3>
140
99
  <p>Updates the output directory across <code>.eleventy.js</code> and all relevant <code>package.json</code> scripts in one shot. The old output folder is deleted automatically.</p>
141
-
142
100
  <blockquote>⚠️ <code>homepage</code> and <code>404</code> are protected — they cannot be created, removed, or renamed via the CLI.</blockquote>
143
101
  </div>
144
102
  </div>
145
- {# STYLING #}
103
+
146
104
  <div id="content-styling" class="tab-content">
147
105
  <div class="markdown-body">
148
106
  <h2>Styling with SCSS</h2>
149
-
150
107
  <h3>Page CSS</h3>
151
108
  <p>Each page has its own SCSS entry point in <code>src/frontend/scss/pages/</code></p>
152
109
  <p>It must contain <code>_root.scss</code> + other modules like <code>_global.scss</code> or any other one that you need and its own specific css rules.</p>
153
110
  <p><code>_root.scss</code> uses <code>@use</code> to enable namespaced access (<code>root.$var</code>); other modules use <code>@import</code> as they don't expose variables.</p>
154
-
155
111
  <h4>examplePage.scss <small>(<code>src/frontend/scss/pages/</code>)</small></h4>
156
112
  <pre><code>//==========================
157
113
  // CSS MODULES IMPORTS
@@ -170,10 +126,8 @@
170
126
  body {
171
127
  background-color: root.$primary;
172
128
  }</code></pre>
173
-
174
129
  <h3>Global Variables</h3>
175
130
  <p>Instead of using <code>:root</code> in your custom modules or pages, the best thing to do is to centralize all your variables in a single file (that will be tree-shaken automatically by Sass).</p>
176
-
177
131
  <h4>_root.scss <small>(<code>src/frontend/scss/modules/</code>)</small></h4>
178
132
  <pre><code>$header-height: 10vh;
179
133
 
@@ -181,18 +135,15 @@ body {
181
135
  header {
182
136
  height: root.$header-height;
183
137
  }</code></pre>
184
-
185
138
  <h3>Scss modules</h3>
186
139
  <p>You can create your custom css modules by creating a new <code>.scss</code> file in <code>src/frontend/scss/modules/</code> (the name of the file must start with <code>_</code>).</p>
187
140
  <p>You can create subfolders if you want to refactor the structure, but be sure to update the relative paths in the pages that import them.</p>
188
-
189
141
  <h4>_yourModule.scss <small>(<code>src/frontend/scss/modules/subfolder/</code>)</small></h4>
190
142
  <pre><code>@use '../root' as root;
191
143
 
192
144
  body {
193
145
  background-color: root.$primary;
194
146
  }</code></pre>
195
-
196
147
  <h4>examplePage.scss</h4>
197
148
  <pre><code>@import "../modules/subfolder/yourModule";
198
149
 
@@ -201,79 +152,44 @@ body {
201
152
  body {
202
153
  color: root.$dark;
203
154
  }</code></pre>
204
-
205
155
  <h4>Pre-existing modules</h4>
206
156
  <table>
207
157
  <thead>
208
- <tr>
209
- <th>File</th>
210
- <th>Purpose</th>
211
- </tr>
158
+ <tr><th>File</th><th>Purpose</th></tr>
212
159
  </thead>
213
160
  <tbody>
214
- <tr>
215
- <td><code>_root.scss</code></td>
216
- <td>Global variables (colors, spacing)</td>
217
- </tr>
218
- <tr>
219
- <td><code>_global.scss</code></td>
220
- <td>Site-wide base rules and frameworks</td>
221
- </tr>
222
- <tr>
223
- <td><code>_typography.scss</code></td>
224
- <td>Font rules</td>
225
- </tr>
226
- <tr>
227
- <td><code>_header.scss</code></td>
228
- <td>Header styles</td>
229
- </tr>
230
- <tr>
231
- <td><code>_footer.scss</code></td>
232
- <td>Footer styles</td>
233
- </tr>
234
- <tr>
235
- <td><code>_mobile.scss</code></td>
236
- <td>Media query rules</td>
237
- </tr>
238
- <tr>
239
- <td><code>_buttons.scss</code></td>
240
- <td>Style and hovers for buttons</td>
241
- </tr>
242
- <tr>
243
- <td><code>_animations.scss</code></td>
244
- <td>Keyframe animations (<code>fade-in</code>, <code>spin</code>)</td>
245
- </tr>
246
- <tr>
247
- <td><code>_notification.scss</code></td>
248
- <td>Notification component style</td>
249
- </tr>
161
+ <tr><td><code>_root.scss</code></td><td>Global variables (colors, spacing)</td></tr>
162
+ <tr><td><code>_global.scss</code></td><td>Site-wide base rules and frameworks</td></tr>
163
+ <tr><td><code>_typography.scss</code></td><td>Font rules</td></tr>
164
+ <tr><td><code>_header.scss</code></td><td>Header styles</td></tr>
165
+ <tr><td><code>_footer.scss</code></td><td>Footer styles</td></tr>
166
+ <tr><td><code>_mobile.scss</code></td><td>Media query rules</td></tr>
167
+ <tr><td><code>_buttons.scss</code></td><td>Style and hovers for buttons</td></tr>
168
+ <tr><td><code>_animations.scss</code></td><td>Keyframe animations (<code>fade-in</code>, <code>spin</code>)</td></tr>
169
+ <tr><td><code>_notification.scss</code></td><td>Notification component style</td></tr>
250
170
  </tbody>
251
171
  </table>
252
-
253
172
  <h3>CSS Framework</h3>
254
173
  <p>Some of the most popular css frameworks that supports scss with modules are already installed in <code>node_modules</code>.</p>
255
174
  <p>You can choose one or none of them (more than 1 works, but you may get in various conflicts).</p>
256
175
  <p>To enable/disable them you have to modify 3 files around the project by just commenting them.</p>
257
-
258
176
  <h4>1. _global.scss <small>(<code>src/frontend/scss/modules/</code>)</small></h4>
259
177
  <pre><code>@import "../modules/frameworks/bootstrap";
260
178
  // @import "../modules/frameworks/bulma";
261
179
  // @import "../modules/frameworks/foundation";
262
180
  // @import "../modules/frameworks/uikit";</code></pre>
263
-
264
181
  <h4>2. base.njk <small>(<code>src/frontend/components/layouts/</code>)</small></h4>
265
- <pre><code>{# Bootstrap JS #}
266
- &lt;script src="/js/bootstrap.bundle.min.js" defer&gt;&lt;/script&gt;
182
+ <pre><code>&lt;!-- Bootstrap --&gt;
183
+ &lt;!-- &lt;script src="/js/bootstrap.bundle.min.js" defer&gt;&lt;/script&gt; --&gt;
267
184
 
268
- {# Foundation JS #}
269
- {# &lt;script src="/js/foundation.min.js" defer&gt;&lt;/script&gt; #}
185
+ &lt;!-- Foundation --&gt;
186
+ &lt;!-- &lt;script src="/js/foundation.min.js" defer&gt;&lt;/script&gt; --&gt;
270
187
 
271
- {# UIkit JS #}
272
- {# &lt;script src="/js/uikit.min.js" defer&gt;&lt;/script&gt; #}
273
- {# &lt;script src="/js/uikit-icons.min.js" defer&gt;&lt;/script&gt; #}
274
-
275
- {# Bulma — no JS needed #}</code></pre>
188
+ &lt;!-- UIkit --&gt;
189
+ &lt;!-- &lt;script src="/js/uikit.min.js" defer&gt;&lt;/script&gt; --&gt;
190
+ &lt;!-- &lt;script src="/js/uikit-icons.min.js" defer&gt;&lt;/script&gt; --&gt;
276
191
 
192
+ &lt;!-- Bulma — no JS needed --&gt;</code></pre>
277
193
  <h4>3. .eleventy.js</h4>
278
194
  <pre><code>eleventyConfig.addPassthroughCopy({
279
195
  // Bootstrap
@@ -289,23 +205,20 @@ body {
289
205
 
290
206
  // Bulma — CSS only, no JS passthrough needed
291
207
  });</code></pre>
292
-
293
208
  <h4>Reducing bundle size</h4>
294
209
  <p>To reduce the bundle size, open the corresponding framework file (<code>src/frontend/scss/modules/frameworks/</code>) and comment out any modules you don't need.</p>
295
210
  <pre><code>@import "bootstrap/scss/card"; // Cards
296
211
  @import "bootstrap/scss/carousel"; // Carousel</code></pre>
297
212
  </div>
298
213
  </div>
299
- {# JAVASCRIPT #}
214
+
300
215
  <div id="content-javascript" class="tab-content">
301
216
  <div class="markdown-body">
302
217
  <h2>JavaScript</h2>
303
-
304
218
  <h3>Page JS</h3>
305
219
  <p>Each page has its own JS entry point in <code>src/frontend/js/pages/</code></p>
306
220
  <p>It is bundled and minified by esbuild and loaded automatically by <code>base.njk</code></p>
307
221
  <p>Import only what the page needs.</p>
308
-
309
222
  <h4>examplePage.js <small>(<code>src/frontend/js/pages/</code>)</small></h4>
310
223
  <pre><code>import { initLangSwitcher } from '../modules/langSwitcher.js';
311
224
  import { showNotification } from '../modules/notification.js';
@@ -315,107 +228,382 @@ body {
315
228
  });
316
229
 
317
230
  showNotification("Page loaded", "success", 3000);</code></pre>
318
-
319
231
  <h3>Modules</h3>
320
232
  <p>Modules live in <code>src/frontend/js/modules/</code>. Some must be called inside <code>DOMContentLoaded</code> as they interact with the DOM; others create elements dynamically and can be called anywhere.</p>
321
-
322
233
  <h4>Call inside <code>DOMContentLoaded</code></h4>
323
234
  <table>
324
235
  <thead>
325
- <tr>
326
- <th>Module</th>
327
- <th>Function</th>
328
- </tr>
236
+ <tr><th>Module</th><th>Function</th></tr>
329
237
  </thead>
330
238
  <tbody>
331
- <tr>
332
- <td><code>modules/langSwitcher.js</code></td>
333
- <td><code>initLangSwitcher()</code></td>
334
- </tr>
335
- <tr>
336
- <td><code>modules/forms/form.js</code></td>
337
- <td><code>initFormListener()</code></td>
338
- </tr>
339
- <tr>
340
- <td><code>modules/forms/textAreaAutoExpand.js</code></td>
341
- <td><code>initTextAreaAutoExpand()</code></td>
342
- </tr>
343
- <tr>
344
- <td><code>modules/forms/normalizePhoneNumber.js</code></td>
345
- <td><code>initNormalizePhoneNumber()</code></td>
346
- </tr>
239
+ <tr><td><code>modules/langSwitcher.js</code></td><td><code>initLangSwitcher()</code></td></tr>
240
+ <tr><td><code>modules/forms/form.js</code></td><td><code>initFormListener()</code></td></tr>
241
+ <tr><td><code>modules/forms/textAreaAutoExpand.js</code></td><td><code>initTextAreaAutoExpand()</code></td></tr>
242
+ <tr><td><code>modules/forms/normalizePhoneNumber.js</code></td><td><code>initNormalizePhoneNumber()</code></td></tr>
347
243
  </tbody>
348
244
  </table>
349
-
350
245
  <h4>Call anywhere</h4>
351
246
  <table>
352
247
  <thead>
353
- <tr>
354
- <th>Module</th>
355
- <th>Function</th>
356
- </tr>
248
+ <tr><th>Module</th><th>Function</th></tr>
357
249
  </thead>
358
250
  <tbody>
359
- <tr>
360
- <td><code>modules/notification.js</code></td>
361
- <td><code>showNotification(text, type, duration)</code></td>
362
- </tr>
251
+ <tr><td><code>modules/notification.js</code></td><td><code>showNotification(text, type, duration)</code></td></tr>
363
252
  </tbody>
364
253
  </table>
365
-
366
254
  <h4><code>showNotification</code> parameters</h4>
367
255
  <table>
368
256
  <thead>
369
- <tr>
370
- <th>Parameter</th>
371
- <th>Type</th>
372
- <th>Default</th>
373
- <th>Values</th>
374
- </tr>
257
+ <tr><th>Parameter</th><th>Type</th><th>Default</th><th>Values</th></tr>
375
258
  </thead>
376
259
  <tbody>
377
- <tr>
378
- <td><code>text</code></td>
379
- <td>string</td>
380
- <td>—</td>
381
- <td>Any string</td>
382
- </tr>
383
- <tr>
384
- <td><code>type</code></td>
385
- <td>string</td>
386
- <td><code>"info"</code></td>
387
- <td><code>"success"</code>, <code>"info"</code>, <code>"error"</code></td>
388
- </tr>
389
- <tr>
390
- <td><code>duration</code></td>
391
- <td>number</td>
392
- <td><code>5000</code></td>
393
- <td>ms, or <code>-1</code> for persistent with spinner</td>
394
- </tr>
260
+ <tr><td><code>text</code></td><td>string</td><td>—</td><td>Any string</td></tr>
261
+ <tr><td><code>type</code></td><td>string</td><td><code>"info"</code></td><td><code>"success"</code>, <code>"info"</code>, <code>"error"</code></td></tr>
262
+ <tr><td><code>duration</code></td><td>number</td><td><code>5000</code></td><td>ms, or <code>-1</code> for persistent with spinner</td></tr>
395
263
  </tbody>
396
264
  </table>
397
-
398
265
  <h3>Adding a module</h3>
399
266
  <p>Create a new <code>.js</code> file in <code>src/frontend/js/modules/</code>. You can organize them into subfolders freely.</p>
400
267
  <p>Use ESM syntax — esbuild handles the bundling:</p>
401
-
402
268
  <pre><code>// _yourModule.js
403
269
  export function yourFunction() {
404
270
  // ...
405
271
  }</code></pre>
406
-
407
272
  <p>Then import it in the pages that need it:</p>
408
-
409
273
  <pre><code>import { yourFunction } from '../modules/yourModule.js';</code></pre>
410
-
411
274
  <blockquote>⚠️ Files inside <code>_tools/</code> run directly in Node.js without a bundler — use CommonJS (<code>require</code> / <code>module.exports</code>) there, not ESM.</blockquote>
412
275
  </div>
413
276
  </div>
414
277
 
415
- <div id="content-deployment" class="tab-content">
278
+ <div id="content-creating-pages" class="tab-content">
279
+ <div class="markdown-body">
280
+ <h2>Creating Pages</h2>
281
+ <p>The recommended way is via the <a href="#" class="nav-to-assistant">Assistant CLI</a>.</p>
282
+ <h3>What gets created</h3>
283
+ <p>For a page named <code>my-page</code>:</p>
284
+ <table>
285
+ <thead>
286
+ <tr><th>File</th><th>Purpose</th></tr>
287
+ </thead>
288
+ <tbody>
289
+ <tr><td><code>src/pages/my-page.njk</code></td><td>Template with front matter</td></tr>
290
+ <tr><td><code>src/scss/pages/myPage.scss</code></td><td>Imports framework + modules</td></tr>
291
+ <tr><td><code>src/js/pages/myPage.js</code></td><td>Imports JS modules</td></tr>
292
+ </tbody>
293
+ </table>
294
+ <h3>Adding content</h3>
295
+ <ol>
296
+ <li>Create a component in <code>src/components/</code> (e.g., <code>_myPage.njk</code>)</li>
297
+ <li>Include it in <code>src/layouts/includes.njk</code> inside the generated <code>elif</code> block:</li>
298
+ </ol>
299
+ <pre><code>&#123;% elif title == "myPage" %&#125;
300
+ &#123;% include "_myPage.njk" %&#125;</code></pre>
301
+ <p>See <a href="components.html">components.md</a> for details.</p>
302
+ <h3>URL and title</h3>
303
+ <p>The URL is the kebab-case name (<code>/my-page/</code>). The <code>title</code> in the front matter is camelCase (<code>myPage</code>) and is used internally to load the correct CSS and JS files — <strong>do not change it</strong>.</p>
304
+ <h3>SEO</h3>
305
+ <p>The CLI creates a stub entry in <code>src/data/site.json</code>. Fill it in:</p>
306
+ <pre><code>"myPage": {
307
+ "seo": {
308
+ "title": "My Page | Site Name",
309
+ "description": "Page description"
310
+ }
311
+ }</code></pre>
312
+ <p>See <a href="head-and-seo.html">head-and-seo.md</a> for all available options.</p>
313
+ </div>
314
+ </div>
315
+
316
+ <div id="content-components" class="tab-content">
416
317
  <div class="markdown-body">
417
- <h2>Deployment</h2>
418
- <p>Extra 2</p>
318
+ <h2>Nunjucks (HTML) Components</h2>
319
+ <h3>What is Nunjucks</h3>
320
+ <p>Nunjucks (<code>.njk</code>) is an HTML file that supports logic like variables, <code>if</code> statements, and <code>for</code> loops. It can extend a base layout and include other <code>.njk</code> components.</p>
321
+ <h3>Create a component</h3>
322
+ <p>Create a new <code>.njk</code> file anywhere inside <code>src/frontend/components/</code>. You can organize them into subfolders freely:</p>
323
+ <pre><code>src/frontend/components/
324
+ ├── global/
325
+ ├── layouts/
326
+ ├── modals/
327
+ │ └── privacyModal.njk
328
+ └── welcome.njk</code></pre>
329
+ <h3>Include a component</h3>
330
+ <p>To render a component inside a page, navigate to <code>src/frontend/components/layouts/</code> and edit <code>includes.njk</code>.</p>
331
+ <h4>includes.njk <small>(<code>src/frontend/components/layouts/</code>)</small></h4>
332
+ <pre><code>&#123;% if title == "homepage" %&#125;
333
+ &#123;% include "welcome.njk" %&#125;
334
+
335
+ &#123;% elif title == "examplePage" %&#125;
336
+ &#123;% include "exampleComponent1.njk" %&#125;
337
+ &#123;% include "subfolder/exampleComponent2.njk" %&#125;
338
+
339
+ &#123;% else %&#125;
340
+ &#123;% include "404/_404.njk" %&#125;
341
+ &#123;&#123; content | safe &#125;&#125;
342
+ &#123;% endif %&#125;</code></pre>
343
+ <p>Add a new <code>&#123;% elif %&#125;</code> block for each page, listing its components in order. If a component lives in a subfolder, specify the relative path accordingly.</p>
344
+ <blockquote>⚠️ A new <code>elif</code> block is automatically added when you create a page via the Assistant CLI.</blockquote>
345
+ <blockquote>⚠️ If you move or delete a component, always update <code>includes.njk</code> or the site will break.</blockquote>
346
+ <h3>Nest components</h3>
347
+ <p>A component can include other components. This is useful for breaking complex sections into smaller, reusable pieces.</p>
348
+ <h4>exampleComponent.njk</h4>
349
+ <pre><code>&lt;section class="hero"&gt;
350
+ &#123;% include "ui/heroTitle.njk" %&#125;
351
+ &#123;% include "ui/heroButton.njk" %&#125;
352
+ &lt;/section&gt;</code></pre>
353
+ <blockquote>The same path rules apply: if the included component is in a subfolder, specify the full relative path.</blockquote>
354
+ <h3>Global components</h3>
355
+ <p>Header and footer live in <code>src/frontend/components/global/</code> and are automatically included in every page via <code>base.njk</code>. Edit them to change the site-wide layout.</p>
356
+ <h3>Site data in components</h3>
357
+ <p>All values defined in <code>src/data/site.json</code> are globally available in every component via <code>&#123;&#123; site.* &#125;&#125;</code>.</p>
358
+ <h4>site.json <small>(<code>src/data/</code>)</small></h4>
359
+ <pre><code>{
360
+ "title": "My Site",
361
+ "logo": "/img/logo.png",
362
+ "legal": {
363
+ "privacy": "/privacy"
364
+ }
365
+ }</code></pre>
366
+ <h4>Usage in any <code>.njk</code> file</h4>
367
+ <pre><code>&lt;p&gt;&#123;&#123; site.title &#125;&#125;&lt;/p&gt;
368
+ &lt;a href="&#123;&#123; site.legal.privacy &#125;&#125;"&gt;Privacy Policy&lt;/a&gt;
369
+ &lt;img src="&#123;&#123; site.logo &#125;&#125;" alt="&#123;&#123; site.title &#125;&#125;"&gt;</code></pre>
370
+ </div>
371
+ </div>
372
+
373
+ <div id="content-head-seo" class="tab-content">
374
+ <div class="markdown-body">
375
+ <h2>Head & SEO</h2>
376
+ <p>This json holds global settings used across all pages in <code>base.njk</code> and other components:</p>
377
+ <h3>site.json <small>(<code>src/frontend/data/</code>)</small></h3>
378
+ <pre><code>{
379
+ "site_name": "Site name",
380
+ "title": "Site title",
381
+ "description": "Site description",
382
+ "keywords": "keyword1, keyword2, keyword3",
383
+ "domain": "yoursite.com",
384
+ "url": "https://yoursite.com",
385
+ "lang": "en",
386
+ "author": "Name and surname",
387
+ "data_bs_theme": "dark",
388
+ "favicon": "/assets/brand/favicon.svg",
389
+ "logo": "/assets/brand/logo.svg"
390
+ }</code></pre>
391
+ <h2>Per-page SEO and CDN</h2>
392
+ <p>Each page entry is keyed by its camelCase <code>title</code> from the front matter.</p>
393
+ <p>If you don't want to use a particular CDN inserting it in <code>base.njk</code> for all pages, you can add extra specific CDN (CSS, JS) by inserting the link in each page of <code>site.json</code> separating them with a <code>,</code> and setting them in <code>""</code>.</p>
394
+ <h3>site.json <small>(<code>src/frontend/data/</code>)</small></h3>
395
+ <pre><code>"pages": {
396
+ "examplePage": {
397
+ "seo": {
398
+ "title": "Example Page",
399
+ "description": "description"
400
+ },
401
+ "cdn": {
402
+ // You can leave the [] empty
403
+ "css": ["https://example1.com/lib.min.css", "https://example2.com/lib.min.css"],
404
+ "js": ["https://example1.com/lib.min.js", "https://example2.com/lib.min.js"]
405
+ }
406
+ }
407
+ }</code></pre>
408
+ <h2>AI & SEO bots</h2>
409
+ <p><code>llms.txt</code> and <code>robots.txt</code> are generated automatically from <code>site.json</code> via their respective <code>.njk</code> files — no manual editing needed.</p>
410
+ <table>
411
+ <thead>
412
+ <tr><th>File</th><th>Purpose</th><th>Reachable at</th></tr>
413
+ </thead>
414
+ <tbody>
415
+ <tr><td><code>llms.njk</code></td><td>Tells AI models what your site is about</td><td><code>yoursite.com/llms.txt</code></td></tr>
416
+ <tr><td><code>robots.njk</code></td><td>Controls search engine crawling</td><td><code>yoursite.com/robots.txt</code></td></tr>
417
+ </tbody>
418
+ </table>
419
+ <p>To customize them, edit <code>src/llms.njk</code> or <code>src/robots.njk</code> directly.</p>
420
+ <h2>Configuration field description</h2>
421
+ <table>
422
+ <thead>
423
+ <tr><th>Field</th><th>Purpose</th></tr>
424
+ </thead>
425
+ <tbody>
426
+ <tr><td><code>site_name</code></td><td>Brand name (used in meta tags)</td></tr>
427
+ <tr><td><code>title</code></td><td>Default page title</td></tr>
428
+ <tr><td><code>description</code></td><td>Default meta description</td></tr>
429
+ <tr><td><code>keywords</code></td><td>Default meta keywords</td></tr>
430
+ <tr><td><code>domain</code> / <code>url</code></td><td>Used for canonical URLs and og:url</td></tr>
431
+ <tr><td><code>lang</code></td><td>HTML <code>lang</code> attribute</td></tr>
432
+ <tr><td><code>author</code></td><td>Meta author tag</td></tr>
433
+ <tr><td><code>data_bs_theme</code></td><td>Bootstrap color scheme (<code>light</code> / <code>dark</code>)</td></tr>
434
+ <tr><td><code>favicon</code></td><td>Path to the favicon</td></tr>
435
+ <tr><td><code>logo</code></td><td>Path to the logo (available as <code>&#123;&#123; site.logo &#125;&#125;</code>)</td></tr>
436
+ </tbody>
437
+ </table>
438
+ </div>
439
+ </div>
440
+
441
+ <div id="content-backend" class="tab-content">
442
+ <div class="markdown-body">
443
+ <h2>Backend</h2>
444
+ <p>The backend is a PHP REST API located in <code>src/backend/</code>, copied to the output directory automatically at build time.</p>
445
+ <h3>Structure</h3>
446
+ <pre><code>src/backend/
447
+ ├── api/
448
+ │ ├── public/ # Endpoints accessible without an API key
449
+ │ └── protected/ # Endpoints requiring X-Api-Key header
450
+ ├── database/
451
+ │ ├── Database.php
452
+ │ ├── models/
453
+ │ └── migrations/
454
+ ├── config.php # Your local config — never commit this
455
+ └── config.example.php</code></pre>
456
+ <h3>Configuration</h3>
457
+ <p><code>config.php</code> works like a <code>.env</code> file — it holds secrets and environment settings that stay local and out of version control.</p>
458
+ <p>Copy <code>config.example.php</code> to <code>config.php</code> and fill in your values:</p>
459
+ <h4>config.php <small>(<code>src/backend/</code>)</small></h4>
460
+ <pre><code>return [
461
+ 'APP_ENV' => 'development', // or 'production'
462
+ 'API_KEY' => 'your-default-key',
463
+
464
+ 'ENDPOINT_KEYS' => [
465
+ 'subfolder/example-protected' => 'specific-key',
466
+ ],
467
+
468
+ 'DB_HOST' => '127.0.0.1',
469
+ 'DB_NAME' => 'example_db',
470
+ 'DB_USER' => 'root',
471
+ 'DB_PASS' => '',
472
+ ];</code></pre>
473
+ <p><code>API_KEY</code> is the fallback key for all protected endpoints. Use <code>ENDPOINT_KEYS</code> to assign a different key to a specific endpoint — for subfolder endpoints, use the relative path as the key.</p>
474
+ <h3>How routing works</h3>
475
+ <p>The file path inside <code>api/</code> maps directly to the URL. Extra URL segments become route parameters available as <code>$requestParams[]</code>.</p>
476
+ <p>Every endpoint file has access to:</p>
477
+ <table>
478
+ <thead>
479
+ <tr><th>Variable</th><th>Description</th></tr>
480
+ </thead>
481
+ <tbody>
482
+ <tr><td><code>$method</code></td><td>HTTP method (<code>GET</code>, <code>POST</code>, <code>PUT</code>, <code>PATCH</code>, <code>DELETE</code>)</td></tr>
483
+ <tr><td><code>$requestParams</code></td><td>Extra URL segments (e.g. <code>/api/posts/42</code> → <code>['42']</code>)</td></tr>
484
+ </tbody>
485
+ </table>
486
+ <h3>Creating a public endpoint</h3>
487
+ <p>Create a <code>.php</code> file anywhere inside <code>api/public/</code></p>
488
+ <pre><code>&lt;?php
489
+ declare(strict_types=1);
490
+
491
+ require_once CORE_PATH . '/modules/Response.php';
492
+
493
+ if ($method !== 'GET') {
494
+ Response::error('Method not allowed', 405);
495
+ }
496
+
497
+ $id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
498
+
499
+ Response::success(['id' => $id]);</code></pre>
500
+ <p>Reachable at <code>/api/posts</code> or <code>/api/posts/42</code></p>
501
+ <h3>Creating a protected endpoint</h3>
502
+ <p>Create a <code>.php</code> file inside <code>api/protected/</code>. The API key check happens automatically before your file runs.</p>
503
+ <pre><code>&lt;?php
504
+ declare(strict_types=1);
505
+
506
+ require_once CORE_PATH . '/modules/Response.php';
507
+
508
+ if ($method !== 'GET') {
509
+ Response::error('Method not allowed', 405);
510
+ }
511
+
512
+ Response::success(['visits' => 1024]);</code></pre>
513
+ <p>To assign a dedicated key, add it to <code>config.php</code>:</p>
514
+ <pre><code>'ENDPOINT_KEYS' => [
515
+ 'admin/stats' => 'secret-stats-key',
516
+ ],</code></pre>
517
+ <h3>The Response helper</h3>
518
+ <pre><code>Response::success($data, $code); // default 200
519
+ Response::error($message, $code, $details); // default 400
520
+ Response::noContent(); // 204</code></pre>
521
+ <h3>Handling multiple methods</h3>
522
+ <pre><code>$id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
523
+ $input = json_decode(file_get_contents('php://input'), true) ?? [];
524
+
525
+ switch ($method) {
526
+ case 'GET':
527
+ Response::success(['id' => $id]);
528
+ break;
529
+
530
+ case 'POST':
531
+ if (empty($input['title'])) Response::error('Missing title', 400);
532
+ Response::success(['message' => 'Created'], 201);
533
+ break;
534
+
535
+ case 'DELETE':
536
+ if (!$id) Response::error('ID required', 400);
537
+ Response::success(['message' => 'Deleted']);
538
+ break;
539
+
540
+ default:
541
+ Response::error('Method not allowed', 405);
542
+ }</code></pre>
543
+ <h3>Using the database</h3>
544
+ <h4>database/models/Post.php</h4>
545
+ <pre><code>&lt;?php
546
+ declare(strict_types=1);
547
+
548
+ require_once __DIR__ . '/../Database.php';
549
+
550
+ class Post {
551
+ private PDO $db;
552
+
553
+ public function __construct() {
554
+ $this->db = Database::getInstance();
555
+ }
556
+
557
+ public function getAll(): array {
558
+ return $this->db->query("SELECT * FROM posts")->fetchAll();
559
+ }
560
+
561
+ public function getById(int $id): ?array {
562
+ $stmt = $this->db->prepare("SELECT * FROM posts WHERE id = :id");
563
+ $stmt->execute(['id' => $id]);
564
+ return $stmt->fetch() ?: null;
565
+ }
566
+
567
+ public function create(string $title): int {
568
+ $stmt = $this->db->prepare("INSERT INTO posts (title) VALUES (:title)");
569
+ $stmt->execute(['title' => htmlspecialchars(strip_tags(trim($title)))]);
570
+ return (int)$this->db->lastInsertId();
571
+ }
572
+ }</code></pre>
573
+ <p>Then use it inside an endpoint:</p>
574
+ <pre><code>require_once __DIR__ . '/../../database/models/Post.php';
575
+
576
+ $post = new Post();
577
+ Response::success($post->getAll());</code></pre>
578
+ <p>Migrations live in <code>database/migrations/</code> as plain SQL files — run them manually against your database.</p>
579
+ <h3>Calling endpoints from the frontend</h3>
580
+ <pre><code>// Public
581
+ const res = await fetch('/api/posts/42');
582
+
583
+ // Protected
584
+ const res = await fetch('/api/admin/stats', {
585
+ headers: { 'X-Api-Key': 'secret-stats-key' }
586
+ });
587
+
588
+ // POST
589
+ const res = await fetch('/api/posts', {
590
+ method: 'POST',
591
+ headers: { 'Content-Type': 'application/json', 'X-Api-Key': 'your-key' },
592
+ body: JSON.stringify({ title: 'Hello world' })
593
+ });</code></pre>
594
+ <h3>Pre-built endpoints</h3>
595
+ <table>
596
+ <thead>
597
+ <tr><th>Route</th><th>Auth</th><th>Methods</th><th>Description</th></tr>
598
+ </thead>
599
+ <tbody>
600
+ <tr><td><code>/api/example-public</code></td><td>No</td><td><code>GET</code></td><td>Smoke test for public routing</td></tr>
601
+ <tr><td><code>/api/subfolder/example-protected</code></td><td>Yes</td><td><code>GET</code></td><td>Smoke test for protected routing</td></tr>
602
+ <tr><td><code>/api/auth/register</code></td><td>No</td><td><code>POST</code></td><td>Register a new user</td></tr>
603
+ <tr><td><code>/api/auth/login</code></td><td>No</td><td><code>POST</code></td><td>Login and retrieve user data</td></tr>
604
+ <tr><td><code>/api/auth-system</code></td><td>Yes</td><td><code>GET POST PUT PATCH DELETE</code></td><td>Full CRUD on users</td></tr>
605
+ </tbody>
606
+ </table>
419
607
  </div>
420
608
  </div>
421
609
  </div>
@@ -425,10 +613,10 @@ body {
425
613
  h1 {
426
614
  text-align: center;
427
615
  font-weight: 400;
428
- .berna-stencil {
429
- color: #42b883;
430
- font-weight: 600;
431
- }
616
+ }
617
+ h1 .berna-stencil {
618
+ color: #42b883;
619
+ font-weight: 600;
432
620
  }
433
621
  .slogan {
434
622
  text-align: center;
@@ -506,14 +694,12 @@ body {
506
694
  flex-wrap: wrap;
507
695
  justify-content: center;
508
696
  margin-bottom: 2rem;
509
-
510
- label {
511
- cursor: pointer;
512
-
513
- input[type="radio"] {
514
- display: none;
515
- }
516
- }
697
+ }
698
+ .guide-filter label {
699
+ cursor: pointer;
700
+ }
701
+ .guide-filter label input[type="radio"] {
702
+ display: none;
517
703
  }
518
704
 
519
705
  .filter-pill {
@@ -637,4 +823,15 @@ body {
637
823
  }
638
824
  });
639
825
  });
826
+ document.querySelector('.nav-to-assistant').addEventListener('click', (e) => {
827
+ e.preventDefault();
828
+
829
+ const assistantRadio = document.querySelector('input[name="guide-filter"][value="assistant"]');
830
+
831
+ if (assistantRadio) {
832
+ assistantRadio.checked = true;
833
+ assistantRadio.dispatchEvent(new Event('change'));
834
+ document.querySelector('.tabs-container');
835
+ }
836
+ });
640
837
  </script>