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.
- package/.eleventy.js +82 -81
- package/CHANGELOG.md +395 -377
- package/LICENSE +169 -169
- package/NOTICE +4 -4
- package/README.md +110 -110
- package/bin/create.js +540 -505
- package/bin/delegate.js +58 -57
- package/bin/nibula.js +302 -302
- package/docs/Assistant CLI.md +65 -65
- package/docs/Backend.md +201 -201
- package/docs/Components.md +140 -140
- package/docs/Creating pages.md +69 -69
- package/docs/Deploy.md +210 -210
- package/docs/Head and SEO.md +139 -139
- package/docs/Javascript.md +126 -126
- package/docs/Styling with SCSS.md +141 -139
- package/nginx.conf +113 -113
- package/package.json +69 -69
- package/src/backend/.htaccess +6 -6
- package/src/backend/_core/composer.json +5 -5
- package/src/backend/_core/composer.lock +492 -492
- package/src/backend/_core/index.js +267 -267
- package/src/backend/_core/index.php +160 -160
- package/src/backend/_core/init.js +52 -52
- package/src/backend/_core/init.php +33 -33
- package/src/backend/_core/modules/RateLimiter.js +58 -58
- package/src/backend/_core/modules/RateLimiter.php +30 -30
- package/src/backend/_core/modules/Response.js +59 -59
- package/src/backend/_core/modules/Response.php +48 -48
- package/src/backend/api/protected/example-protected.js +23 -23
- package/src/backend/api/protected/example-protected.php +16 -16
- package/src/backend/api/public/example-public.js +24 -24
- package/src/backend/api/public/example-public.php +16 -16
- package/src/backend/database/Database.js +46 -46
- package/src/backend/database/Database.php +23 -23
- package/src/backend/example.config.js +37 -37
- package/src/backend/example.config.php +37 -37
- package/src/backend/package.json +17 -17
- package/src/backend/web.config +43 -43
- package/src/frontend/.htaccess +51 -51
- package/src/frontend/404.njk +17 -17
- package/src/frontend/assets/brand/favicon.svg +54 -54
- package/src/frontend/assets/brand/logo.svg +54 -54
- package/src/frontend/components/global/footer.njk +25 -25
- package/src/frontend/components/global/header.njk +6 -6
- package/src/frontend/components/welcome.njk +115 -115
- package/src/frontend/index.njk +8 -8
- package/src/frontend/js/global.js +10 -10
- package/src/frontend/js/modules/exampleModule.js +2 -2
- package/src/frontend/js/pages/404.js +8 -8
- package/src/frontend/js/pages/homepage.js +8 -8
- package/src/frontend/layouts/base.njk +135 -136
- package/src/frontend/layouts/page-components.njk +13 -13
- package/src/frontend/llms.njk +17 -17
- package/src/frontend/robots.njk +7 -7
- package/src/frontend/scss/_global.scss +46 -46
- package/src/frontend/scss/_root.scss +13 -13
- package/src/frontend/scss/modules/_animations.scss +24 -24
- package/src/frontend/scss/modules/_footer.scss +28 -28
- package/src/frontend/scss/modules/_header.scss +26 -26
- package/src/frontend/scss/modules/_mobile.scss +29 -29
- package/src/frontend/scss/modules/_typography.scss +18 -18
- package/src/frontend/scss/modules/frameworks/_bootstrap.scss +109 -109
- package/src/frontend/scss/modules/frameworks/_bulma.scss +108 -108
- package/src/frontend/scss/modules/frameworks/_foundation.scss +137 -137
- package/src/frontend/scss/modules/frameworks/_uikit.scss +109 -109
- package/src/frontend/scss/pages/404.scss +27 -27
- package/src/frontend/scss/pages/homepage.scss +22 -22
- package/src/frontend/sitemap.njk +17 -17
- package/src/frontend/ts/global.ts +11 -11
- package/src/frontend/ts/modules/exampleModule.ts +2 -2
- package/src/frontend/ts/pages/404.ts +8 -8
- package/src/frontend/ts/pages/homepage.ts +8 -8
- package/src/frontend/web.config +51 -51
- package/tools/assistant.js +122 -122
- package/tools/buildJs.js +39 -39
- package/tools/cleanOutput.js +23 -23
- package/tools/cli/prompt.js +40 -40
- package/tools/cli/ui.js +87 -87
- package/tools/config/messages.json +80 -80
- package/tools/config/settings.json +130 -130
- package/tools/lib/colors.js +17 -17
- package/tools/lib/files.js +75 -75
- package/tools/lib/logger.js +22 -22
- package/tools/lib/outputPath.js +141 -141
- package/tools/lib/pageActions.js +146 -146
- package/tools/lib/pageArtifacts.js +46 -46
- package/tools/lib/pageComponents.js +88 -88
- package/tools/lib/pagesData.js +87 -87
- package/tools/lib/paths.js +63 -63
- package/tools/lib/project.js +54 -54
- package/tools/lib/text.js +47 -47
- package/tools/lib/validation.js +40 -40
- package/tools/res/templates/template.js +7 -7
- package/tools/res/templates/template.njk +8 -8
- package/tools/res/templates/template.scss +22 -22
- package/tools/res/templates/template.ts +8 -8
- package/tsconfig.json +24 -24
- package/src/frontend/scss/pages/prova.scss +0 -23
package/docs/Head and SEO.md
CHANGED
|
@@ -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.
|
package/docs/Javascript.md
CHANGED
|
@@ -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
|
```
|