nibula 1.6.0 → 1.6.2

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.
Files changed (99) hide show
  1. package/.eleventy.js +82 -81
  2. package/CHANGELOG.md +395 -377
  3. package/LICENSE +169 -169
  4. package/NOTICE +4 -4
  5. package/README.md +110 -110
  6. package/bin/create.js +540 -505
  7. package/bin/delegate.js +58 -57
  8. package/bin/nibula.js +302 -302
  9. package/docs/Assistant CLI.md +65 -65
  10. package/docs/Backend.md +201 -201
  11. package/docs/Components.md +140 -140
  12. package/docs/Creating pages.md +69 -69
  13. package/docs/Deploy.md +210 -210
  14. package/docs/Head and SEO.md +139 -139
  15. package/docs/Javascript.md +126 -126
  16. package/docs/Styling with SCSS.md +141 -139
  17. package/nginx.conf +113 -113
  18. package/package.json +69 -69
  19. package/src/backend/.htaccess +6 -6
  20. package/src/backend/_core/composer.json +5 -5
  21. package/src/backend/_core/composer.lock +492 -492
  22. package/src/backend/_core/index.js +267 -267
  23. package/src/backend/_core/index.php +160 -160
  24. package/src/backend/_core/init.js +52 -52
  25. package/src/backend/_core/init.php +33 -33
  26. package/src/backend/_core/modules/RateLimiter.js +58 -58
  27. package/src/backend/_core/modules/RateLimiter.php +30 -30
  28. package/src/backend/_core/modules/Response.js +59 -59
  29. package/src/backend/_core/modules/Response.php +48 -48
  30. package/src/backend/api/protected/example-protected.js +23 -23
  31. package/src/backend/api/protected/example-protected.php +16 -16
  32. package/src/backend/api/public/example-public.js +24 -24
  33. package/src/backend/api/public/example-public.php +16 -16
  34. package/src/backend/database/Database.js +46 -46
  35. package/src/backend/database/Database.php +23 -23
  36. package/src/backend/example.config.js +37 -37
  37. package/src/backend/example.config.php +37 -37
  38. package/src/backend/package.json +17 -17
  39. package/src/backend/web.config +43 -43
  40. package/src/frontend/.htaccess +51 -51
  41. package/src/frontend/404.njk +17 -17
  42. package/src/frontend/assets/brand/favicon.svg +54 -54
  43. package/src/frontend/assets/brand/logo.svg +54 -54
  44. package/src/frontend/components/global/footer.njk +25 -25
  45. package/src/frontend/components/global/header.njk +6 -6
  46. package/src/frontend/components/welcome.njk +115 -115
  47. package/src/frontend/index.njk +8 -8
  48. package/src/frontend/js/global.js +10 -10
  49. package/src/frontend/js/modules/exampleModule.js +2 -2
  50. package/src/frontend/js/pages/404.js +8 -8
  51. package/src/frontend/js/pages/homepage.js +8 -8
  52. package/src/frontend/layouts/base.njk +135 -136
  53. package/src/frontend/layouts/page-components.njk +13 -13
  54. package/src/frontend/llms.njk +17 -17
  55. package/src/frontend/robots.njk +7 -7
  56. package/src/frontend/scss/_global.scss +46 -46
  57. package/src/frontend/scss/_root.scss +13 -13
  58. package/src/frontend/scss/modules/_animations.scss +24 -24
  59. package/src/frontend/scss/modules/_footer.scss +28 -28
  60. package/src/frontend/scss/modules/_header.scss +26 -26
  61. package/src/frontend/scss/modules/_mobile.scss +29 -29
  62. package/src/frontend/scss/modules/_typography.scss +18 -18
  63. package/src/frontend/scss/modules/frameworks/_bootstrap.scss +109 -109
  64. package/src/frontend/scss/modules/frameworks/_bulma.scss +108 -108
  65. package/src/frontend/scss/modules/frameworks/_foundation.scss +137 -137
  66. package/src/frontend/scss/modules/frameworks/_uikit.scss +109 -109
  67. package/src/frontend/scss/pages/404.scss +27 -27
  68. package/src/frontend/scss/pages/homepage.scss +22 -22
  69. package/src/frontend/sitemap.njk +17 -17
  70. package/src/frontend/ts/global.ts +11 -11
  71. package/src/frontend/ts/modules/exampleModule.ts +2 -2
  72. package/src/frontend/ts/pages/404.ts +8 -8
  73. package/src/frontend/ts/pages/homepage.ts +8 -8
  74. package/src/frontend/web.config +51 -51
  75. package/tools/assistant.js +122 -122
  76. package/tools/buildJs.js +39 -39
  77. package/tools/cleanOutput.js +23 -23
  78. package/tools/cli/prompt.js +40 -40
  79. package/tools/cli/ui.js +87 -87
  80. package/tools/config/messages.json +80 -80
  81. package/tools/config/settings.json +130 -130
  82. package/tools/lib/colors.js +17 -17
  83. package/tools/lib/files.js +75 -75
  84. package/tools/lib/logger.js +22 -22
  85. package/tools/lib/outputPath.js +141 -141
  86. package/tools/lib/pageActions.js +146 -146
  87. package/tools/lib/pageArtifacts.js +46 -46
  88. package/tools/lib/pageComponents.js +88 -88
  89. package/tools/lib/pagesData.js +87 -87
  90. package/tools/lib/paths.js +63 -63
  91. package/tools/lib/project.js +54 -54
  92. package/tools/lib/text.js +47 -47
  93. package/tools/lib/validation.js +40 -40
  94. package/tools/res/templates/template.js +7 -7
  95. package/tools/res/templates/template.njk +8 -8
  96. package/tools/res/templates/template.scss +22 -22
  97. package/tools/res/templates/template.ts +8 -8
  98. package/tsconfig.json +24 -24
  99. package/src/frontend/scss/pages/prova.scss +0 -23
@@ -1,140 +1,140 @@
1
- # Head & SEO
2
-
3
- Global settings live in `src/frontend/data/pages.json` and are available everywhere via `{{ pages.* }}`.
4
-
5
- ## Global fields
6
-
7
- ```json
8
- {
9
- "site_name": "Site name",
10
- "title": "Site title",
11
- "description": "Site description",
12
- "keywords": "keyword1, keyword2, keyword3",
13
- "domain": "yoursite.com",
14
- "url": "https://yoursite.com",
15
- "lang": "en",
16
- "author": "Name and surname",
17
- "theme": "dark",
18
- "logo": "/assets/brand/logo.svg",
19
- "legal": {
20
- "privacy": "",
21
- "cookie": "",
22
- "cookieControls": "",
23
- "terms": "",
24
- "copyright": {
25
- "year": "2026",
26
- "text": "Copyright text"
27
- }
28
- }
29
- }
30
- ```
31
-
32
- | Field | Purpose |
33
- |---|---|
34
- | `site_name` | Brand name (used in meta tags and JSON-LD) |
35
- | `title` | Default page title — fallback when a page has no `seo.title` |
36
- | `description` | Default meta description — fallback for pages |
37
- | `keywords` | Default meta keywords — fallback for pages |
38
- | `domain` / `url` | Canonical URLs, `og:url`, JSON-LD |
39
- | `lang` | HTML `lang` attribute |
40
- | `author` | Meta author and JSON-LD author |
41
- | `theme` | Site color scheme (`light` / `dark`) — sets `data-theme`, `data-bs-theme` and `color-scheme` |
42
- | `theme_color` | Browser UI / PWA bar color — should match the chosen `theme` |
43
- | `logo` | Path to the logo, also used as social image |
44
- | `legal.privacy` / `cookie` / `cookieControls` / `terms` | Legal page URLs |
45
- | `legal.copyright.year` / `text` | Footer copyright |
46
-
47
- ## Per-page SEO and CDN
48
-
49
- Per-page settings live in `src/frontend/data/pages.json`, available via `{{ pages.* }}`. Each page is keyed by its camelCase `title` from the front matter, at the root of the file:
50
-
51
- ```json
52
- {
53
- "examplePage": {
54
- "seo": {
55
- "title": "Example Page",
56
- "description": "Description",
57
- "keywords": "",
58
- "noindex": false,
59
- "canonical": ""
60
- },
61
- "cdn": {
62
- "css": ["https://example.com/lib.min.css"],
63
- "js": ["https://example.com/lib.min.js"]
64
- }
65
- }
66
- }
67
- ```
68
-
69
- | Field | Purpose | If empty / absent |
70
- |---|---|---|
71
- | `seo.title` | Page title | Falls back to global `title` |
72
- | `seo.description` | Meta description | Falls back to global `description` |
73
- | `seo.keywords` | Meta keywords | Falls back to global `keywords` |
74
- | `seo.noindex` | If `true`, emits `<meta name="robots" content="noindex, follow">` and drops the page from the sitemap | Page is indexable |
75
- | `seo.canonical` | Absolute canonical URL override | Computed as `url + page.url` |
76
- | `cdn.css` / `cdn.js` | Extra per-page CDN links | No extra links loaded |
77
-
78
- ## Fallback logic
79
-
80
- Global values are defaults, not duplicates. A page value is used when present; otherwise the global one applies:
81
-
82
- ```njk
83
- {{ pageData.seo.title or title or site.title }}
84
- {{ pageData.seo.description or site.description }}
85
- {{ pageData.seo.keywords or site.keywords }}
86
- ```
87
-
88
- `noindex` and `canonical` have no global default: `noindex` defaults to indexable behavior, `canonical` is computed from the URL. They exist per-page only.
89
-
90
- ## Theme
91
-
92
- `theme` is a single fixed value that drives the whole site:
93
-
94
- ```njk
95
- <html data-theme="{{ site.theme }}" data-bs-theme="{{ site.theme }}">
96
- <meta name="color-scheme" content="{{ site.theme }}">
97
- <meta name="theme-color" content="{{ site.theme_color }}">
98
- ```
99
-
100
- | Attribute | Purpose |
101
- |---|---|
102
- | `data-theme` | Your own hook — target it in SCSS to define theme variables |
103
- | `data-bs-theme` | Bootstrap's color mode. Ignored (harmless) if you use another framework |
104
- | `color-scheme` | Standard CSS — makes scrollbars, form controls and autofill follow the theme |
105
- | `theme-color` | Color of the browser UI bar on mobile |
106
-
107
- Keep `theme` and `theme_color` consistent: a `dark` theme with a white `theme_color` gives a white browser bar over a dark page.
108
-
109
- See the SCSS docs for how to hook your variables to `data-theme`.
110
-
111
- ## Favicon
112
-
113
- The three favicon tags are hardcoded in `base.njk`. To change the icons, replace the files in `src/frontend/assets/brand/` keeping the same names:
114
-
115
- ```html
116
- <link rel="icon" type="image/svg+xml" href="{{ '/assets/brand/favicon.svg' | url }}">
117
- <link rel="icon" type="image/png" sizes="32x32" href="{{ '/assets/brand/favicon-32.png' | url }}">
118
- <link rel="apple-touch-icon" sizes="180x180" href="{{ '/assets/brand/apple-touch-icon.png' | url }}">
119
- ```
120
-
121
- | File | Purpose |
122
- |---|---|
123
- | `favicon.svg` | Modern browsers, scales to any size |
124
- | `favicon-48.png` | Fallback for older browsers and some crawlers |
125
- | `favicon-32.png` | Fallback for older browsers and some crawlers |
126
- | `apple-touch-icon.png` | iOS home screen icon — 180×180, must be opaque (iOS renders transparency as black) |
127
-
128
- > ⚠️ Without `apple-touch-icon.png`, iOS uses a screenshot of the page as the home screen icon.
129
-
130
- ## AI & SEO bots
131
-
132
- `llms.txt`, `robots.txt` and `sitemap.xml` are generated from `.njk` templates and are publicly reachable (they must not be blocked by the server config).
133
-
134
- | File | Purpose | Reachable at |
135
- |---|---|---|
136
- | `llms.njk` | Tells AI models what the site is about | `yoursite.com/llms.txt` |
137
- | `robots.njk` | Controls search-engine crawling | `yoursite.com/robots.txt` |
138
- | `sitemap.njk` | Lists indexable pages (skips drafts, 404, and `noindex`) | `yoursite.com/sitemap.xml` |
139
-
1
+ # Head & SEO
2
+
3
+ Global settings live in `src/frontend/data/pages.json` and are available everywhere via `{{ pages.* }}`.
4
+
5
+ ## Global fields
6
+
7
+ ```json
8
+ {
9
+ "site_name": "Site name",
10
+ "title": "Site title",
11
+ "description": "Site description",
12
+ "keywords": "keyword1, keyword2, keyword3",
13
+ "domain": "yoursite.com",
14
+ "url": "https://yoursite.com",
15
+ "lang": "en",
16
+ "author": "Name and surname",
17
+ "theme": "dark",
18
+ "logo": "/assets/brand/logo.svg",
19
+ "legal": {
20
+ "privacy": "",
21
+ "cookie": "",
22
+ "cookieControls": "",
23
+ "terms": "",
24
+ "copyright": {
25
+ "year": "2026",
26
+ "text": "Copyright text"
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ | Field | Purpose |
33
+ |---|---|
34
+ | `site_name` | Brand name (used in meta tags and JSON-LD) |
35
+ | `title` | Default page title — fallback when a page has no `seo.title` |
36
+ | `description` | Default meta description — fallback for pages |
37
+ | `keywords` | Default meta keywords — fallback for pages |
38
+ | `domain` / `url` | Canonical URLs, `og:url`, JSON-LD |
39
+ | `lang` | HTML `lang` attribute |
40
+ | `author` | Meta author and JSON-LD author |
41
+ | `theme` | Site color scheme (`light` / `dark`) — sets `data-theme`, `data-bs-theme` and `color-scheme` |
42
+ | `theme_color` | Browser UI / PWA bar color — should match the chosen `theme` |
43
+ | `logo` | Path to the logo, also used as social image |
44
+ | `legal.privacy` / `cookie` / `cookieControls` / `terms` | Legal page URLs |
45
+ | `legal.copyright.year` / `text` | Footer copyright |
46
+
47
+ ## Per-page SEO and CDN
48
+
49
+ Per-page settings live in `src/frontend/data/pages.json`, available via `{{ pages.* }}`. Each page is keyed by its camelCase `title` from the front matter, at the root of the file:
50
+
51
+ ```json
52
+ {
53
+ "examplePage": {
54
+ "seo": {
55
+ "title": "Example Page",
56
+ "description": "Description",
57
+ "keywords": "",
58
+ "noindex": false,
59
+ "canonical": ""
60
+ },
61
+ "cdn": {
62
+ "css": ["https://example.com/lib.min.css"],
63
+ "js": ["https://example.com/lib.min.js"]
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ | Field | Purpose | If empty / absent |
70
+ |---|---|---|
71
+ | `seo.title` | Page title | Falls back to global `title` |
72
+ | `seo.description` | Meta description | Falls back to global `description` |
73
+ | `seo.keywords` | Meta keywords | Falls back to global `keywords` |
74
+ | `seo.noindex` | If `true`, emits `<meta name="robots" content="noindex, follow">` and drops the page from the sitemap | Page is indexable |
75
+ | `seo.canonical` | Absolute canonical URL override | Computed as `url + page.url` |
76
+ | `cdn.css` / `cdn.js` | Extra per-page CDN links | No extra links loaded |
77
+
78
+ ## Fallback logic
79
+
80
+ Global values are defaults, not duplicates. A page value is used when present; otherwise the global one applies:
81
+
82
+ ```njk
83
+ {{ pageData.seo.title or title or site.title }}
84
+ {{ pageData.seo.description or site.description }}
85
+ {{ pageData.seo.keywords or site.keywords }}
86
+ ```
87
+
88
+ `noindex` and `canonical` have no global default: `noindex` defaults to indexable behavior, `canonical` is computed from the URL. They exist per-page only.
89
+
90
+ ## Theme
91
+
92
+ `theme` is a single fixed value that drives the whole site:
93
+
94
+ ```njk
95
+ <html data-theme="{{ site.theme }}" data-bs-theme="{{ site.theme }}">
96
+ <meta name="color-scheme" content="{{ site.theme }}">
97
+ <meta name="theme-color" content="{{ site.theme_color }}">
98
+ ```
99
+
100
+ | Attribute | Purpose |
101
+ |---|---|
102
+ | `data-theme` | Your own hook — target it in SCSS to define theme variables |
103
+ | `data-bs-theme` | Bootstrap's color mode. Ignored (harmless) if you use another framework |
104
+ | `color-scheme` | Standard CSS — makes scrollbars, form controls and autofill follow the theme |
105
+ | `theme-color` | Color of the browser UI bar on mobile |
106
+
107
+ Keep `theme` and `theme_color` consistent: a `dark` theme with a white `theme_color` gives a white browser bar over a dark page.
108
+
109
+ See the SCSS docs for how to hook your variables to `data-theme`.
110
+
111
+ ## Favicon
112
+
113
+ The three favicon tags are hardcoded in `base.njk`. To change the icons, replace the files in `src/frontend/assets/brand/` keeping the same names:
114
+
115
+ ```html
116
+ <link rel="icon" type="image/svg+xml" href="{{ '/assets/brand/favicon.svg' | url }}">
117
+ <link rel="icon" type="image/png" sizes="32x32" href="{{ '/assets/brand/favicon-32.png' | url }}">
118
+ <link rel="apple-touch-icon" sizes="180x180" href="{{ '/assets/brand/apple-touch-icon.png' | url }}">
119
+ ```
120
+
121
+ | File | Purpose |
122
+ |---|---|
123
+ | `favicon.svg` | Modern browsers, scales to any size |
124
+ | `favicon-48.png` | Fallback for older browsers and some crawlers |
125
+ | `favicon-32.png` | Fallback for older browsers and some crawlers |
126
+ | `apple-touch-icon.png` | iOS home screen icon — 180×180, must be opaque (iOS renders transparency as black) |
127
+
128
+ > ⚠️ Without `apple-touch-icon.png`, iOS uses a screenshot of the page as the home screen icon.
129
+
130
+ ## AI & SEO bots
131
+
132
+ `llms.txt`, `robots.txt` and `sitemap.xml` are generated from `.njk` templates and are publicly reachable (they must not be blocked by the server config).
133
+
134
+ | File | Purpose | Reachable at |
135
+ |---|---|---|
136
+ | `llms.njk` | Tells AI models what the site is about | `yoursite.com/llms.txt` |
137
+ | `robots.njk` | Controls search-engine crawling | `yoursite.com/robots.txt` |
138
+ | `sitemap.njk` | Lists indexable pages (skips drafts, 404, and `noindex`) | `yoursite.com/sitemap.xml` |
139
+
140
140
  To customize, edit `src/frontend/llms.njk` or `src/frontend/robots.njk` directly.
@@ -1,127 +1,127 @@
1
- # JavaScript
2
-
3
- > Examples use JavaScript, but everything applies equally to TypeScript. The only difference is the file extension (`.ts` instead of `.js`), that imports do **not** include the extension, and that paths use `src/frontend/ts/` instead of `src/frontend/js/`.
4
-
5
- ## Page JS
6
-
7
- Each page has its own JS entry point in `src/frontend/js/pages/`, bundled and minified by esbuild and loaded automatically by `base.njk`.
8
-
9
- Import only what the page needs.
10
-
11
- ### examplePage.js <small>(`src/frontend/js/pages/`)</small>
12
-
13
- ```js
14
- import '../global.js';
15
-
16
- // import { initExampleModule } from '../modules/exampleModule.js';
17
-
18
- document.addEventListener("DOMContentLoaded", () => {
19
- // initExampleModule();
20
- });
21
-
22
- // Page logic here
23
- ```
24
-
25
- ## Global JS
26
-
27
- Some code has to run on every page: a header menu, a theme toggle, a cookie banner. Repeating it in every page entry point means you'll eventually forget it on one of them, and the bug only shows up on that single page.
28
-
29
- That's what `global.js` is for. It lives in `src/frontend/js/`, every page entry point imports it, and it takes care of running the shared modules.
30
-
31
- ### global.js <small>(`src/frontend/js/`)</small>
32
-
33
- ```js
34
- // import { initExampleModule } from './exampleModule.js';
35
-
36
- function initGlobal() {
37
- // initExampleModule();
38
- }
39
-
40
- // Global logic here
41
-
42
- // Do not touch
43
- // This instruction starts global.js itself
44
- document.addEventListener('DOMContentLoaded', initGlobal);
45
- ```
46
-
47
- Pages import it without curly braces, since there is nothing to export — the module runs on its own as soon as it's part of the bundle:
48
-
49
- ```js
50
- import '../global.js';
51
- ```
52
-
53
- Note that the import path inside `global.js` is `'./exampleModule.js'` and not `'../modules/exampleModule.js'`: `global.js` already sits in the modules folder, next to the modules it imports.
54
-
55
- ### A concrete example: the header menu
56
-
57
- A responsive header has a burger button that opens the navigation on small screens. The header is included in `base.njk`, so it's on every page — a textbook case for `global.js`.
58
-
59
- Write the behaviour as a normal module:
60
-
61
- ### header.js <small>(`src/frontend/js/modules/`)</small>
62
- ```js
63
- export function initHeader() {
64
- const toggle = document.querySelector('.menu-toggle');
65
- const menu = document.querySelector('.menu');
66
-
67
- if (!toggle || !menu) return;
68
-
69
- toggle.addEventListener('click', () => {
70
- menu.classList.toggle('is-open');
71
- });
72
- }
73
- ```
74
-
75
- Then wire it up once, in `global.js`:
76
-
77
- ```js
78
- import { initHeader } from './modules/header.js';
79
-
80
- function initGlobal() {
81
- initHeader();
82
- }
83
-
84
- // Do not touch
85
- // This instruction starts global.js itself
86
- document.addEventListener('DOMContentLoaded', initGlobal);
87
- ```
88
-
89
- The burger now works on every page, and no page entry point had to change. Adding another global behaviour later is two lines: one import, one call inside `initGlobal()`.
90
-
91
- The early `return` matters: if a page has no header, the module does nothing instead of throwing an error that would stop your other scripts.
92
-
93
- ### Global or page module?
94
-
95
- Use `global.js` for behaviour tied to something present on every page — anything included in `base.njk`, essentially.
96
-
97
- Keep it in the page entry point when only one or two pages need it. Everything imported by `global.js` ends up in *every* page bundle, so a heavy module used by a single page belongs in that page instead.
98
-
99
- ## Modules
100
-
101
- Modules live in `src/frontend/js/modules/`. Modules that interact with the DOM must be called inside `DOMContentLoaded`; others can be called anywhere.
102
-
103
- ## Adding a module
104
-
105
- Create a new `.js` file in `src/frontend/js/modules/`. Subfolders are allowed.
106
-
107
- Use ESM syntax — esbuild handles the bundling:
108
-
109
- ### exampleModule.js <small>(`src/frontend/js/modules/`)</small>
110
-
111
- ```js
112
- export function exampleModule() {
113
- // Module logic here
114
- }
115
- ```
116
-
117
- Then import it in the pages that need it:
118
-
119
- ```js
120
- import { exampleModule } from '../modules/exampleModule.js';
121
- ```
122
-
123
- In TypeScript, omit the extension:
124
-
125
- ```ts
126
- import { exampleModule } from '../modules/exampleModule';
1
+ # JavaScript
2
+
3
+ > Examples use JavaScript, but everything applies equally to TypeScript. The only difference is the file extension (`.ts` instead of `.js`), that imports do **not** include the extension, and that paths use `src/frontend/ts/` instead of `src/frontend/js/`.
4
+
5
+ ## Page JS
6
+
7
+ Each page has its own JS entry point in `src/frontend/js/pages/`, bundled and minified by esbuild and loaded automatically by `base.njk`.
8
+
9
+ Import only what the page needs.
10
+
11
+ ### examplePage.js <small>(`src/frontend/js/pages/`)</small>
12
+
13
+ ```js
14
+ import '../global.js';
15
+
16
+ // import { initExampleModule } from '../modules/exampleModule.js';
17
+
18
+ document.addEventListener("DOMContentLoaded", () => {
19
+ // initExampleModule();
20
+ });
21
+
22
+ // Page logic here
23
+ ```
24
+
25
+ ## Global JS
26
+
27
+ Some code has to run on every page: a header menu, a theme toggle, a cookie banner. Repeating it in every page entry point means you'll eventually forget it on one of them, and the bug only shows up on that single page.
28
+
29
+ That's what `global.js` is for. It lives in `src/frontend/js/`, every page entry point imports it, and it takes care of running the shared modules.
30
+
31
+ ### global.js <small>(`src/frontend/js/`)</small>
32
+
33
+ ```js
34
+ // import { initExampleModule } from './exampleModule.js';
35
+
36
+ function initGlobal() {
37
+ // initExampleModule();
38
+ }
39
+
40
+ // Global logic here
41
+
42
+ // Do not touch
43
+ // This instruction starts global.js itself
44
+ document.addEventListener('DOMContentLoaded', initGlobal);
45
+ ```
46
+
47
+ Pages import it without curly braces, since there is nothing to export — the module runs on its own as soon as it's part of the bundle:
48
+
49
+ ```js
50
+ import '../global.js';
51
+ ```
52
+
53
+ Note that the import path inside `global.js` is `'./exampleModule.js'` and not `'../modules/exampleModule.js'`: `global.js` already sits in the modules folder, next to the modules it imports.
54
+
55
+ ### A concrete example: the header menu
56
+
57
+ A responsive header has a burger button that opens the navigation on small screens. The header is included in `base.njk`, so it's on every page — a textbook case for `global.js`.
58
+
59
+ Write the behaviour as a normal module:
60
+
61
+ ### header.js <small>(`src/frontend/js/modules/`)</small>
62
+ ```js
63
+ export function initHeader() {
64
+ const toggle = document.querySelector('.menu-toggle');
65
+ const menu = document.querySelector('.menu');
66
+
67
+ if (!toggle || !menu) return;
68
+
69
+ toggle.addEventListener('click', () => {
70
+ menu.classList.toggle('is-open');
71
+ });
72
+ }
73
+ ```
74
+
75
+ Then wire it up once, in `global.js`:
76
+
77
+ ```js
78
+ import { initHeader } from './modules/header.js';
79
+
80
+ function initGlobal() {
81
+ initHeader();
82
+ }
83
+
84
+ // Do not touch
85
+ // This instruction starts global.js itself
86
+ document.addEventListener('DOMContentLoaded', initGlobal);
87
+ ```
88
+
89
+ The burger now works on every page, and no page entry point had to change. Adding another global behaviour later is two lines: one import, one call inside `initGlobal()`.
90
+
91
+ The early `return` matters: if a page has no header, the module does nothing instead of throwing an error that would stop your other scripts.
92
+
93
+ ### Global or page module?
94
+
95
+ Use `global.js` for behaviour tied to something present on every page — anything included in `base.njk`, essentially.
96
+
97
+ Keep it in the page entry point when only one or two pages need it. Everything imported by `global.js` ends up in *every* page bundle, so a heavy module used by a single page belongs in that page instead.
98
+
99
+ ## Modules
100
+
101
+ Modules live in `src/frontend/js/modules/`. Modules that interact with the DOM must be called inside `DOMContentLoaded`; others can be called anywhere.
102
+
103
+ ## Adding a module
104
+
105
+ Create a new `.js` file in `src/frontend/js/modules/`. Subfolders are allowed.
106
+
107
+ Use ESM syntax — esbuild handles the bundling:
108
+
109
+ ### exampleModule.js <small>(`src/frontend/js/modules/`)</small>
110
+
111
+ ```js
112
+ export function exampleModule() {
113
+ // Module logic here
114
+ }
115
+ ```
116
+
117
+ Then import it in the pages that need it:
118
+
119
+ ```js
120
+ import { exampleModule } from '../modules/exampleModule.js';
121
+ ```
122
+
123
+ In TypeScript, omit the extension:
124
+
125
+ ```ts
126
+ import { exampleModule } from '../modules/exampleModule';
127
127
  ```