nibula 1.6.2 → 2.0.1

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 CHANGED
@@ -27,8 +27,11 @@ module.exports = function (eleventyConfig) {
27
27
  copyRecursiveSync("src/backend", `${OUTPUT_DIR}/backend`);
28
28
  });
29
29
 
30
- eleventyConfig.addPassthroughCopy("src/frontend/.htaccess");
31
- eleventyConfig.addPassthroughCopy("src/frontend/web.config");
30
+ eleventyConfig.addPassthroughCopy({
31
+ "src/frontend/hosting/.htaccess": ".htaccess",
32
+ "src/frontend/hosting/web.config": "web.config",
33
+ });
34
+
32
35
  eleventyConfig.addPassthroughCopy("src/frontend/assets");
33
36
  eleventyConfig.addPassthroughCopy("src/frontend/robots.txt");
34
37
 
package/CHANGELOG.md CHANGED
@@ -5,13 +5,120 @@ All notable changes to Nibula are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [1.6.2] - 2026-08-07
8
+ ## [2.0.1] - 2026-08-08
9
9
 
10
10
  ### Added
11
- - `nib --version` (also `-v`, `v`, `version`) prints the version number on its own line. Inside a project it reports the project's own copy, which is the one actually running the command.
11
+ - `docs/Components.md` documents custom layouts. `base.njk` was presented as the
12
+ only layout there is, so a page that doesn't fit the standard shell — a landing
13
+ page with no header or footer, a full-screen login — had no documented way out.
14
+ Copying `base.njk` and pointing a route at the copy was already supported;
15
+ nothing said so.
16
+ - `docs/Head and SEO.md` shows where the tags are actually written, including the
17
+ `pageKey` lookup that ties a route's `title` to its record in `pages.json`, and
18
+ warns against removing the `url` filter from asset paths — it is what keeps a
19
+ site published under a subpath from 404ing every stylesheet and script.
20
+ - `docs/Creating pages.md` covers the `layout` front matter field, which the file
21
+ showed in every example without ever explaining.
12
22
 
13
23
  ### Fixed
14
- [...le quattro voci di prima, invariate...]
24
+ - `src/frontend/ts/global.ts` imported `exampleModule` for real while leaving the
25
+ call commented out, so a new TypeScript project started with an unused import
26
+ and a stale path in the comment above it. It now matches `global.js`, where the
27
+ import is commented too.
28
+ - The `site.json` snippet in `docs/Components.md` was not valid JSON: the opening
29
+ and two closing braces were missing and three `//` comments sat inside it, so
30
+ copying it into the file broke the build.
31
+ - `docs/Head and SEO.md` listed `favicon-48.png` among the favicon files. No tag
32
+ in `base.njk` loads it.
33
+
34
+ ### Changed
35
+ - `docs/Creating pages.md` no longer re-explains component includes and the
36
+ `pages.json` stub, which are documented in full elsewhere and had started to
37
+ drift.
38
+ - Nunjucks snippets in `docs/Components.md` are tagged as such instead of as
39
+ JavaScript.
40
+ - `docs/Project structure.md` points at `layouts/` for pages that need their own
41
+ skeleton.
42
+
43
+ ## [2.0.0] - 2026-08-07
44
+
45
+ ### Breaking
46
+
47
+ Existing projects keep running the Nibula version they were built with. These
48
+ steps apply only when you run `npm install nibula@latest` inside a project. Do
49
+ them in one pass, before the next build.
50
+
51
+ - **`page-components.njk` no longer exists.** Its `elif` chain is replaced by
52
+ includes written directly in each route. For every page, move the includes
53
+ from its `elif` block into the matching file in `src/frontend/routes/`, then
54
+ delete `src/frontend/layouts/page-components.njk`.
55
+ - **Every route's layout becomes `base.njk`.** Change `layout: page-components.njk`
56
+ to `layout: base.njk` in the front matter of each file in `routes/`.
57
+ - **`index.njk` and `404.njk` move into `routes/`.** They sat at the root of
58
+ `src/frontend/`; every page now lives in one place.
59
+ - **`llms.njk`, `robots.njk` and `sitemap.njk` move into `src/frontend/indexing/`.**
60
+ Their `permalink` values are unchanged, so the published URLs stay the same.
61
+ - **`.htaccess` and `web.config` move into `src/frontend/hosting/`.** The build
62
+ still places them in the root of the output folder, so what you deploy is
63
+ unchanged. `nginx.conf` stays at the project root: it is installed on the
64
+ server by hand, not published with the site.
65
+
66
+ ### Added
67
+
68
+ - `docs/Project structure.md` maps the whole project and lists where to go for
69
+ each kind of change. There was no overview of the layout anywhere, so the two
70
+ new folders — and the older ones — had to be inferred from the file tree.
71
+ - `src/frontend/components/not-found.njk` holds the 404 page markup, included by
72
+ `routes/404.njk`.
73
+
74
+ ### Changed
75
+
76
+ - **`llms.txt` is generated from page data instead of being hardcoded.** It
77
+ listed the homepage and nothing else, so every other page was invisible to AI
78
+ crawlers unless you edited the template by hand. It now walks the same
79
+ collection `sitemap.njk` does, reading each page's title and description from
80
+ `pages.json` and skipping drafts, the 404 page and anything marked `noindex`.
81
+ Creating a page with the assistant is enough to have it listed.
82
+ - **Components are included in the route that renders them.** Composition lived
83
+ in a single shared layout, so adding a page meant appending a branch to a file
84
+ that grew with every page, and reading one page's structure meant scrolling
85
+ past all the others. A route now includes what it needs, and what a page
86
+ contains is visible in the page itself.
87
+ - **The assistant no longer edits Nunjucks templates.** Creating, renaming and
88
+ removing a page used to rewrite `page-components.njk` with regular expressions
89
+ against the template's syntax. That step is gone; the assistant now touches
90
+ only files it fully controls.
91
+ - **Documentation catches up with the code.** `Head & SEO` opened by pointing at
92
+ `pages.json` and `{{ pages.* }}` for the global settings, which have lived in
93
+ `site.json` since 1.6.0, and still listed `legal.cookieControls`, removed in
94
+ the same release. `JavaScript` claimed `global.js` sits in the modules folder,
95
+ which stopped being true when it moved up a level — the surrounding example
96
+ already contradicted it. `Styling with SCSS` referred to the Eleventy config
97
+ as `eleventy.config.js`.
98
+ - New pages are scaffolded with a commented example include instead of a warning
99
+ not to edit the file.
100
+
101
+ ### Removed
102
+
103
+ - `src/frontend/layouts/page-components.njk`, and with it the `pageComponents`
104
+ configuration block, its six log messages, and its entry in the project's
105
+ required-files check.
106
+ - The include of `404/_404.njk`, a component that was never shipped. It survived
107
+ only because of `ignore missing`, which silently swallowed it on every build.
108
+
109
+ ### Notes
110
+
111
+ - `routes/` is no longer created on demand or removed when empty: with
112
+ `index.njk` and `404.njk` inside, it always exists. The same on-demand handling
113
+ still applies to `scss/pages/` and `js/pages/`.
114
+ - Writing markup directly in a route works and is fine for a simple page.
115
+ Components remain the recommendation when there is something to reuse, not a
116
+ requirement.
117
+
118
+ ## [1.6.2] - 2026-08-07
119
+
120
+ ### Added
121
+ - `nib --version` (also `-v`, `v`, `ver`, `version`) prints the version number on its own line. Inside a project it reports the project's own copy, which is the one actually running the command.
15
122
 
16
123
  ### Changed
17
124
  - `docs/Styling with SCSS.md` now matches the template: the Nunjucks snippets include the `url` filter and the config file is named `.eleventy.js`.
package/bin/nibula.js CHANGED
@@ -293,6 +293,14 @@ async function main() {
293
293
  usage(pkg.version);
294
294
  break;
295
295
  }
296
+ case 'version':
297
+ case 'v':
298
+ case 'ver':
299
+ case '-v':
300
+ case '--version': {
301
+ console.log(pkg.version);
302
+ break;
303
+ }
296
304
  default:
297
305
  console.error(`${color.red}\nUnknown command:${color.reset} ${cmd}`);
298
306
  usage(pkg.version);
@@ -33,7 +33,7 @@ For a page named `my-page`, the following files are created:
33
33
  | `src/frontend/js/pages/myPage.js` | JS entry point |
34
34
  | `src/frontend/routes/my-page.njk` | Nunjucks template |
35
35
 
36
- It also adds an `elif` block in `page-components.njk` and a stub entry in `pages.json`:
36
+ It also adds a stub entry in `pages.json`:
37
37
 
38
38
  ```json
39
39
  "myPage": {
@@ -51,13 +51,17 @@ It also adds an `elif` block in `page-components.njk` and a stub entry in `pages
51
51
  }
52
52
  ```
53
53
 
54
+ The route is created with an empty body and a commented example — you decide which components go in it. See **Components** DOC file.
55
+
54
56
  ## Remove page
55
57
 
56
- Deletes all source files for the page and cleans up the output directory, `page-components.njk`, and `pages.json`.
58
+ Deletes all source files for the page, cleans up the output directory, and removes the record from `pages.json`.
57
59
 
58
60
  ## Rename page
59
61
 
60
- Renames all three source files, updates the `elif` block in `page-components.njk`, and renames the record in `pages.json`. Nothing inside the record is touched — your SEO title, description and CDN links stay exactly as you wrote them.
62
+ Renames all three source files, updates the `permalink` in the route's front matter, and renames the record in `pages.json`. Nothing inside the record is touched — your SEO title, description and CDN links stay exactly as you wrote them.
63
+
64
+ The includes you wrote inside the route move with the file, since the route itself is renamed rather than rewritten.
61
65
 
62
66
  ## Configure output path
63
67
 
package/docs/Backend.md CHANGED
@@ -42,6 +42,23 @@ src/backend/
42
42
  > the matching `example.config.*`, and are git-ignored so your secrets stay
43
43
  > local. If `config.*` is missing (e.g. after a fresh clone), copy the example.
44
44
 
45
+ ### Installing Composer packages (PHP)
46
+
47
+ Backend dependencies live in `src/backend/_core`, next to `composer.json`.
48
+ Install from there, not from the project root:
49
+
50
+ ```bash
51
+ cd src/backend/_core
52
+ composer require vendor/package
53
+ ```
54
+
55
+ `_core/init.php` already requires Composer's autoloader, so an installed package
56
+ is usable in your endpoints straight away — there is no extra `require` to add.
57
+
58
+ Packages land in `src/backend/_core/vendor/`, which is git-ignored. After a fresh
59
+ clone that folder is missing and the backend won't start at all: run
60
+ `composer install` from `src/backend/_core` to restore it.
61
+
45
62
  ## How routing works
46
63
 
47
64
  The file path inside `api/` maps directly to the URL. The `public` / `protected`
@@ -11,44 +11,45 @@ Create a new `.njk` file anywhere inside `src/frontend/components/`. You can org
11
11
  ```
12
12
  src/frontend/components/
13
13
  ├── global/
14
- ├── layouts/
14
+ │ ├── header.njk
15
+ │ └── footer.njk
15
16
  ├── modals/
16
- │ └── privacyModal.njk # You can move it to a modals/subfolder
17
+ │ └── privacyModal.njk
18
+ ├── not-found.njk
17
19
  ├── welcome.njk
18
20
  ```
19
21
 
20
22
  ## Include a component
21
23
 
22
- To render a component inside a page, navigate to `src/frontend/layouts/` and edit `page-components.njk`
24
+ Components are included in the page that renders them. Open the page's file in `src/frontend/routes/` and write the includes in its body
23
25
 
24
- ### page-components.njk <small>(`src/frontend/layouts/`)</small>
26
+ ### example-page.njk <small>(`src/frontend/routes/`)</small>
25
27
 
26
- ```js
27
- {% if title == "homepage" %}
28
- {% include "welcome.njk" %}
28
+ ```njk
29
+ ---
30
+ title: "examplePage"
31
+ permalink: "/example-page/"
32
+ layout: base.njk
33
+ ---
29
34
 
30
- {% elif title == "examplePage" %}
31
35
  {% include "example-component-1.njk" %}
32
36
  {% include "subfolder/example-component-2.njk" %}
33
-
34
- {% else %}
35
- {% include "404/_404.njk" %}
36
- {{ content | safe }}
37
- {% endif %}
38
37
  ```
39
38
 
40
- Add a new `{% elif %}` block for each page, listing its components in order. If a component lives in a subfolder, specify the relative path accordingly
39
+ Components render in the order you list them. If a component lives in a subfolder, specify the relative path accordingly
40
+
41
+ Paths are resolved from `src/frontend/components/`, so you never write that part — `"welcome.njk"`, not `"components/welcome.njk"`
41
42
 
42
- > ⚠️ A new `elif` block is automatically added when you create a page via the Assistant CLI
43
+ > ⚠️ If you move or delete a component, update every route that includes it or the build will fail
43
44
 
44
- > ⚠️ If you move or delete a component, always update `page-components.njk` or the site will break
45
+ Writing HTML directly in the route works too, and is perfectly fine for a short page. Components are the recommendation when there is something to reuse or when a page grows past a screenful — not a rule
45
46
 
46
47
  ## Nest components
47
48
 
48
49
  A component can include other components. This is useful for breaking complex sections into smaller, reusable pieces.
49
50
 
50
51
  ### exampleComponent.njk
51
- ```js
52
+ ```njk
52
53
  <section class="hero">
53
54
  {% include "hero/hero-title.njk" %}
54
55
  {% include "hero/hero-button.njk" %}
@@ -61,33 +62,87 @@ A component can include other components. This is useful for breaking complex se
61
62
 
62
63
  Header and footer live in `src/frontend/components/global/` and are automatically included in every page via `base.njk`. Edit them to change the site-wide layout
63
64
 
65
+ ## Custom layouts
66
+
67
+ `base.njk` is the default layout, but it isn't the only one you can have. Any
68
+ `.njk` file in `src/frontend/layouts/` is a layout, and each route picks its own.
69
+
70
+ The usual reason is a page that doesn't fit the standard shell: a landing page
71
+ with no header or footer, a full-screen login, a "coming soon" holding page.
72
+
73
+ Copy `base.njk`, rename it, and change only what you need — that way you keep the
74
+ head, the SEO tags and the CSS/JS wiring, which is the part you don't want to
75
+ rewrite:
76
+
77
+ ```
78
+ src/frontend/layouts/
79
+ ├── base.njk
80
+ └── custom.njk
81
+ ```
82
+
83
+ ### custom.njk <small>(`src/frontend/layouts/`)</small>
84
+
85
+ Same as `base.njk`, without the two global includes:
86
+
87
+ ```njk
88
+ <body>
89
+ <main>
90
+ {{ content | safe }}
91
+ </main>
92
+ </body>
93
+ ```
94
+
95
+ Then point the route at it:
96
+
97
+ ### landing.njk <small>(`src/frontend/routes/`)</small>
98
+
99
+ ```njk
100
+ ---
101
+ title: "landing"
102
+ permalink: "/landing/"
103
+ layout: custom.njk
104
+ ---
105
+ ```
106
+
107
+ Paths are resolved from `src/frontend/layouts/`, so you write `"custom.njk"`, not
108
+ `"layouts/custom.njk"` — the same rule as components.
109
+
110
+ > ⚠️ A layout you copy is a copy: a later fix to `base.njk` won't reach it. Keep
111
+ > the differences to the minimum, and check your custom layouts when you change
112
+ > something in the head.
113
+
64
114
  ## Site data in components
65
115
 
66
116
  All values defined in `src/frontend/data/site.json` are globally available in every component via `{{ site.* }}`
67
117
 
68
118
  ### site.json <small>(`src/frontend/data/`)</small>
69
119
  ```json
70
- "site_name": "Site name",
71
- "title": "Site title",
72
- "description": "Site description",
73
- "keywords": "keyword1, keyword2, keyword3",
74
- "domain": "yoursite.com",
75
- "url": "https://yoursite.com",
76
- "lang": "en",
77
- "author": "Name and surname",
78
- "logo": "/assets/brand/logo.svg",
79
- "legal": {
80
- "privacy": "", // This is supposed to contain a link
81
- "cookie": "", // This is supposed to contain a link
82
- "terms": "", // This is supposed to contain a link
83
- "copyright": {
84
- "year": "2026",
85
- "text": "All rights reserved."
120
+ {
121
+ "site_name": "Site name",
122
+ "title": "Site title",
123
+ "description": "Site description",
124
+ "keywords": "keyword1, keyword2, keyword3",
125
+ "domain": "yoursite.com",
126
+ "url": "https://yoursite.com",
127
+ "lang": "en",
128
+ "author": "Name and surname",
129
+ "logo": "/assets/brand/logo.svg",
130
+ "legal": {
131
+ "privacy": "",
132
+ "cookie": "",
133
+ "terms": "",
134
+ "copyright": {
135
+ "year": "2026",
136
+ "text": "All rights reserved."
137
+ }
86
138
  }
139
+ }
87
140
  ```
88
141
 
142
+ `legal.privacy`, `legal.cookie` and `legal.terms` hold the URLs of your legal pages — the footer links to them. See **Head & SEO** DOC file for what every field does
143
+
89
144
  ### Usage in any `.njk` file
90
- ```js
145
+ ```njk
91
146
  <p>{{ site.title }}</p>
92
147
  <a href="{{ site.legal.privacy }}">Privacy Policy</a>
93
148
  <img src="{{ site.logo }}" alt="{{ site.title }}">
@@ -107,7 +162,7 @@ Create `test.json`, put anything you want inside it, and every key is reachable
107
162
  ```
108
163
 
109
164
  ### Usage in any `.njk` file
110
- ```js
165
+ ```njk
111
166
  <p>{{ test.testMessage }}</p>
112
167
  ```
113
168
 
@@ -130,7 +185,7 @@ Values can be anything JSON allows, so a list lets you loop instead of copying m
130
185
  }
131
186
  ```
132
187
 
133
- ```js
188
+ ```njk
134
189
  {% for link in test.links %}
135
190
  <a href="{{ link.url }}">{{ link.label }}</a>
136
191
  {% endfor %}
@@ -13,21 +13,21 @@ For a page named `my-page`:
13
13
  | `src/frontend/scss/pages/myPage.scss` | Imports framework + modules |
14
14
  | `src/frontend/js/pages/myPage.js` | Imports JS/TS modules |
15
15
 
16
+ It also adds a stub entry in `src/frontend/data/pages.json` for the page's SEO and CDN links — fill it in as described in the **Head & SEO** DOC file.
17
+
16
18
  ## Adding content
17
19
 
18
- 1. Create a component in `src/frontend/components/` (e.g. `my-component.njk`)
19
- 2. Include it in `src/frontend/layouts/page-components.njk` inside the generated `elif` block:
20
+ The route is created empty. Write your HTML in its body, or include components — see the **Components** DOC file.
20
21
 
21
- ```njk
22
- {% elif title == "myPage" %}
23
- {% include "my-component.njk" ignore missing %}
24
- ```
22
+ ## URL and title
25
23
 
26
- See **Components** DOC file for more info
24
+ The URL is the kebab-case name (`/my-page/`). The `title` in the front matter is camelCase (`myPage`) and is used internally to load the correct CSS and JS files, and to look up the page's record in `pages.json` — do not change it.
27
25
 
28
- ## URL and title
26
+ ## Layout
27
+
28
+ `layout` decides the HTML skeleton the page is rendered into. New pages get `base.njk`, which brings the head, the SEO tags, the header and the footer.
29
29
 
30
- The URL is the kebab-case name (`/my-page/`). The `title` in the front matter is camelCase (`myPage`) and is used internally to load the correct CSS and JS files — do not change it.
30
+ A page that needs a different shell — a landing page with no header, for instance — points at a layout of its own. See **Components** DOC file, *Custom layouts*.
31
31
 
32
32
  ## Subpages (nested URLs)
33
33
 
@@ -36,8 +36,8 @@ To create a URL like `domain.it/about/team`, edit the `permalink` in `src/fronte
36
36
  ```njk
37
37
  ---
38
38
  title: "team"
39
- permalink: "about/team/"
40
- layout: page-components.njk
39
+ permalink: "/about/team/"
40
+ layout: base.njk
41
41
  ---
42
42
  ```
43
43
 
@@ -47,24 +47,4 @@ The parent path (`about`) does **not** need to exist as a real page — it's jus
47
47
  |---|---|---|
48
48
  | `/team/` | `/team/` | `routes/team.njk` |
49
49
  | `/about/team/` | `/about/team/` | `routes/team.njk` |
50
- | `/company/about/team/` | `/company/about/team/` | `routes/team.njk` |
51
-
52
- ## SEO
53
-
54
- The CLI creates a stub entry in `src/frontend/data/pages.json`. Fill it in:
55
-
56
- ```json
57
- "myPage": {
58
- "seo": {
59
- "title": "My Page",
60
- "description": "Description",
61
- "keywords": "",
62
- "noindex": false,
63
- "canonical": ""
64
- },
65
- "cdn": {
66
- "css": [],
67
- "js": []
68
- }
69
- }
70
- ```
50
+ | `/company/about/team/` | `/company/about/team/` | `routes/team.njk` |
package/docs/Deploy.md CHANGED
@@ -39,6 +39,16 @@ dependencies.
39
39
  **Node backend:** Node.js 18+ and shell access to run a persistent process — a
40
40
  VPS you control.
41
41
 
42
+ ## Editing the server config
43
+
44
+ `.htaccess` and `web.config` are in the root of `out`, ready to upload. To change
45
+ a rule, edit the source in `src/frontend/hosting/` — a change made directly in
46
+ `out` is lost on the next build.
47
+
48
+ `nginx.conf` works differently: it sits in the project root and is **not** part
49
+ of `out`, because nginx reads no per-directory config. You install it on the
50
+ server by hand, as described below.
51
+
42
52
  ---
43
53
 
44
54
  ## Shared hosting (Apache)
@@ -1,6 +1,6 @@
1
1
  # Head & SEO
2
2
 
3
- Global settings live in `src/frontend/data/pages.json` and are available everywhere via `{{ pages.* }}`.
3
+ Global settings live in `src/frontend/data/site.json` and are available everywhere via `{{ site.* }}`.
4
4
 
5
5
  ## Global fields
6
6
 
@@ -15,11 +15,11 @@ Global settings live in `src/frontend/data/pages.json` and are available everywh
15
15
  "lang": "en",
16
16
  "author": "Name and surname",
17
17
  "theme": "dark",
18
+ "theme_color": "#2a2150",
18
19
  "logo": "/assets/brand/logo.svg",
19
20
  "legal": {
20
21
  "privacy": "",
21
22
  "cookie": "",
22
- "cookieControls": "",
23
23
  "terms": "",
24
24
  "copyright": {
25
25
  "year": "2026",
@@ -41,7 +41,7 @@ Global settings live in `src/frontend/data/pages.json` and are available everywh
41
41
  | `theme` | Site color scheme (`light` / `dark`) — sets `data-theme`, `data-bs-theme` and `color-scheme` |
42
42
  | `theme_color` | Browser UI / PWA bar color — should match the chosen `theme` |
43
43
  | `logo` | Path to the logo, also used as social image |
44
- | `legal.privacy` / `cookie` / `cookieControls` / `terms` | Legal page URLs |
44
+ | `legal.privacy` / `cookie` / `terms` | Legal page URLs |
45
45
  | `legal.copyright.year` / `text` | Footer copyright |
46
46
 
47
47
  ## Per-page SEO and CDN
@@ -68,13 +68,15 @@ Per-page settings live in `src/frontend/data/pages.json`, available via `{{ page
68
68
 
69
69
  | Field | Purpose | If empty / absent |
70
70
  |---|---|---|
71
- | `seo.title` | Page title | Falls back to global `title` |
72
- | `seo.description` | Meta description | Falls back to global `description` |
71
+ | `seo.title` | Page title, and the page's label in `llms.txt` | Falls back to global `title` |
72
+ | `seo.description` | Meta description, and the page's summary in `llms.txt` | Falls back to global `description` |
73
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 |
74
+ | `seo.noindex` | If `true`, emits `<meta name="robots" content="noindex, follow">` and drops the page from both the sitemap and `llms.txt` | Page is indexable |
75
75
  | `seo.canonical` | Absolute canonical URL override | Computed as `url + page.url` |
76
76
  | `cdn.css` / `cdn.js` | Extra per-page CDN links | No extra links loaded |
77
77
 
78
+ The assistant creates the stub entry when you add a page; a page left with the placeholder description will advertise itself that way to AI crawlers.
79
+
78
80
  ## Fallback logic
79
81
 
80
82
  Global values are defaults, not duplicates. A page value is used when present; otherwise the global one applies:
@@ -121,20 +123,34 @@ The three favicon tags are hardcoded in `base.njk`. To change the icons, replace
121
123
  | File | Purpose |
122
124
  |---|---|
123
125
  | `favicon.svg` | Modern browsers, scales to any size |
124
- | `favicon-48.png` | Fallback for older browsers and some crawlers |
125
126
  | `favicon-32.png` | Fallback for older browsers and some crawlers |
126
127
  | `apple-touch-icon.png` | iOS home screen icon — 180×180, must be opaque (iOS renders transparency as black) |
127
128
 
128
129
  > ⚠️ Without `apple-touch-icon.png`, iOS uses a screenshot of the page as the home screen icon.
129
130
 
131
+ ## Where the tags are written
132
+
133
+ All of the above is rendered by `src/frontend/layouts/base.njk`, which reads the page's record with:
134
+
135
+ ```njk
136
+ {% set pageKey = title %}
137
+ {% set pageData = pages[pageKey] %}
138
+ ```
139
+
140
+ Edit the template when you need a tag the boilerplate doesn't cover — a font `<link>`, an analytics snippet. For everything listed here, change the values in `site.json` and `pages.json` instead.
141
+
142
+ > ⚠️ Asset paths in `base.njk` go through the `url` filter. It makes them work when the site is published under a subpath — GitHub Pages being the usual case. Removing it 404s every stylesheet and script there.
143
+
130
144
  ## AI & SEO bots
131
145
 
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).
146
+ `llms.txt`, `robots.txt` and `sitemap.xml` are generated from `.njk` templates in `src/frontend/indexing/` and are publicly reachable (they must not be blocked by the server config).
133
147
 
134
148
  | File | Purpose | Reachable at |
135
149
  |---|---|---|
136
- | `llms.njk` | Tells AI models what the site is about | `yoursite.com/llms.txt` |
150
+ | `llms.njk` | Describes the site and lists its pages for AI models | `yoursite.com/llms.txt` |
137
151
  | `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` |
152
+ | `sitemap.njk` | Lists indexable pages | `yoursite.com/sitemap.xml` |
153
+
154
+ `llms.njk` and `sitemap.njk` build their lists the same way: they walk every page, then skip drafts, the 404 page, and anything with `seo.noindex` set to `true`. Creating a page with the assistant is enough to have it appear in both — there is no separate list to maintain.
139
155
 
140
- To customize, edit `src/frontend/llms.njk` or `src/frontend/robots.njk` directly.
156
+ To customize the wording or the fixed sections, edit `src/frontend/indexing/llms.njk` or `src/frontend/indexing/robots.njk` directly.
@@ -31,7 +31,7 @@ That's what `global.js` is for. It lives in `src/frontend/js/`, every page entry
31
31
  ### global.js <small>(`src/frontend/js/`)</small>
32
32
 
33
33
  ```js
34
- // import { initExampleModule } from './exampleModule.js';
34
+ // import { initExampleModule } from './modules/exampleModule.js';
35
35
 
36
36
  function initGlobal() {
37
37
  // initExampleModule();
@@ -50,7 +50,7 @@ Pages import it without curly braces, since there is nothing to export — the m
50
50
  import '../global.js';
51
51
  ```
52
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.
53
+ Note the difference in import paths. A page entry point sits inside `js/pages/`, so it reaches a module with `'../modules/exampleModule.js'`. `global.js` sits directly in `js/`, one level up, so the same module is `'./modules/exampleModule.js'`.
54
54
 
55
55
  ### A concrete example: the header menu
56
56
 
@@ -0,0 +1,65 @@
1
+ # Project Structure
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
+ Nibula ships a fixed layout so you always know where to put your hands. Inside these folders you're free to organize as you like — subfolders in `components/`, `modules/` and `api/` are all fair game.
6
+
7
+ ## The tree
8
+
9
+ ```
10
+ your-project/
11
+ ├── .eleventy.js # Eleventy configuration
12
+ ├── nginx.conf # nginx site file
13
+ ├── package.json
14
+ ├── out/ # Build output — never edit, never commit
15
+ └── src/
16
+ ├── backend/ # REST API — see docs/Backend.md
17
+ └── frontend/
18
+ ├── assets/ # Images, fonts, brand files
19
+ │ └── brand/ # Favicons, logo etc
20
+ ├── components/ # Reusable .njk fragments
21
+ │ └── global/ # Header and footer, included in every page
22
+ ├── data/ # site.json and pages.json
23
+ ├── hosting/ # .htaccess and web.config
24
+ ├── indexing/ # llms, robots and sitemap templates
25
+ ├── js/ # global.js, modules/, pages/
26
+ ├── layouts/ # base.njk — the HTML skeleton every page extends
27
+ ├── routes/ # One file per page
28
+ └── scss/ # _root.scss, _global.scss, modules/, pages/
29
+ ```
30
+
31
+ ## Where things happen
32
+
33
+ | I want to... | Go to |
34
+ |---|---|
35
+ | Add a page | `nib cli` → creates the files in `routes/`, `scss/pages/`, `js/pages/` |
36
+ | Write the content of a page | `routes/` |
37
+ | Build a reusable block of markup | `components/` |
38
+ | Change the header or footer | `components/global/` |
39
+ | Style one page | `scss/pages/` |
40
+ | Style every page | `scss/_global.scss` |
41
+ | Define a variable used across the site | `scss/_root.scss` |
42
+ | Run a script on one page | `js/pages/` |
43
+ | Run a script everywhere | `js/global.js` |
44
+ | Change the site name, URL, author | `data/site.json` |
45
+ | Change a page's SEO | `data/pages.json` |
46
+ | Edit the HTML `<head>` | `layouts/base.njk` |
47
+ | Give one page a different skeleton | `layouts/` — add your own, see docs/Components.md |
48
+ | Add an API endpoint | `backend/api/` |
49
+
50
+ ## Two folders worth a note
51
+
52
+ **`out/`** is generated by every build. Editing anything in it means losing the change the next time you run `nib run` or `nib build`. Always edit the source in `src/` — including `.htaccess` and `web.config`, whose sources live in `src/frontend/hosting/`.
53
+
54
+ **`data/`** feeds the build but is never published: it holds the full page map, including pages you marked `noindex`, so it stays out of the output on purpose.
55
+
56
+ ## What you can rearrange
57
+
58
+ Subfolders are allowed wherever content is yours to organize:
59
+
60
+ - `components/` — nest as deep as you like; includes take the path relative to `components/`
61
+ - `scss/modules/` and `js/modules/` — same idea, remember to update the relative import paths
62
+ - `layouts/` — extra layouts alongside `base.njk`; routes pick one by filename
63
+ - `backend/api/public/` and `protected/` — subfolders become part of the URL
64
+
65
+ The folders in the tree above are the part to leave alone: paths are wired into `.eleventy.js`, the build scripts and the assistant, so renaming one breaks the build.
@@ -111,7 +111,7 @@ To enable/disable them you have to modify 3 files around the project by just com
111
111
  {# Bulma — no JS needed #}
112
112
  ```
113
113
 
114
- ### 3. eleventy.js
114
+ ### 3. .eleventy.js <small>(project root)</small>
115
115
 
116
116
  ```javascript
117
117
  eleventyConfig.addPassthroughCopy({
@@ -130,7 +130,9 @@ eleventyConfig.addPassthroughCopy({
130
130
  });
131
131
  ```
132
132
 
133
- > ⚠️ Changes to `eleventy.js` are not picked up while the dev server is running — restart it.
133
+ > ⚠️ Changes to `.eleventy.js` are not picked up while the dev server is running — restart it.
134
+
135
+ > These three files are set up for you when you pick a framework during `nib new`. You only need to edit them by hand if you change your mind later.
134
136
 
135
137
  ### Reducing bundle size
136
138
 
package/nginx.conf CHANGED
@@ -29,7 +29,7 @@ server {
29
29
 
30
30
  # HTTPS (443) configuration
31
31
  server {
32
- ### Essentials
32
+
33
33
  listen 443 ssl;
34
34
  listen [::]:443 ssl;
35
35
 
@@ -40,13 +40,13 @@ server {
40
40
  root /var/www/SITE_FOLDER;
41
41
  index index.html;
42
42
 
43
- ### Certificate
43
+ # Certificate
44
44
  ssl_certificate /etc/letsencrypt/live/YOUR_CERTIFICATE/fullchain.pem;
45
45
  ssl_certificate_key /etc/letsencrypt/live/YOUR_CERTIFICATE/privkey.pem;
46
46
  include /etc/letsencrypt/options-ssl-nginx.conf;
47
47
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
48
48
 
49
- ### Security headers
49
+ # Security headers
50
50
  add_header X-Content-Type-Options "nosniff" always;
51
51
  add_header X-Frame-Options "SAMEORIGIN" always;
52
52
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
@@ -55,10 +55,12 @@ server {
55
55
  # 31536000 once stable; "; includeSubDomains" needs HTTPS on all subdomains.
56
56
  add_header Strict-Transport-Security "max-age=300" always;
57
57
 
58
+ # Hide Nginx version in error pages and Server HTTP header to avoid vulnerabilities
58
59
  server_tokens off;
60
+
59
61
  autoindex off;
60
62
 
61
- ### Blocked paths
63
+ # Blocked paths (redirect to 404.html)
62
64
  error_page 403 404 /404.html;
63
65
 
64
66
  location ^~ /backend/ {
@@ -77,7 +79,7 @@ server {
77
79
  return 404;
78
80
  }
79
81
 
80
- ### API backend — Node primary, PHP fallback
82
+ # API backend — Node primary, PHP fallback
81
83
  location ^~ /api/ {
82
84
  proxy_pass http://127.0.0.1:3000;
83
85
  proxy_http_version 1.1;
@@ -102,12 +104,12 @@ server {
102
104
  fastcgi_pass unix:/run/php/php-fpm.sock;
103
105
  }
104
106
 
105
- ### Static files
107
+ # Static files
106
108
  location / {
107
109
  try_files $uri $uri/ $uri.html =404;
108
110
  }
109
111
 
110
- ### Error page
112
+ # Error page
111
113
  location = /404.html {
112
114
  internal;
113
115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nibula",
3
- "version": "1.6.2",
3
+ "version": "2.0.1",
4
4
  "description": "A beginner-friendly static site generator built on Eleventy that stays close to vanilla HTML, CSS and JS — with a page-management CLI, an optional PHP backend, and SEO files generated for you.",
5
5
  "keywords": [
6
6
  "11ty",
@@ -0,0 +1,4 @@
1
+ <div class="fade-in not-found">
2
+ <h1>Oops! Page not found</h1>
3
+ <a href="/">Return to homepage</a>
4
+ </div>
@@ -0,0 +1,10 @@
1
+ ---
2
+ title: "404"
3
+ permalink: "404.html"
4
+ layout: base.njk
5
+ ---
6
+
7
+ <!-- !IMPORTANT -->
8
+ <!-- To keep order, the best practice is to create a new component.njk file in components/ and include it here-->
9
+
10
+ {% include "not-found.njk" ignore missing %}
@@ -0,0 +1,10 @@
1
+ ---
2
+ title: "homepage"
3
+ permalink: "/"
4
+ layout: base.njk
5
+ ---
6
+
7
+ <!-- !IMPORTANT -->
8
+ <!-- To keep order, the best practice is to create a new component.njk file in components/ and include it here-->
9
+
10
+ {% include "welcome.njk" ignore missing %}
@@ -1,5 +1,4 @@
1
- // import { initExampleModule } from "/exampleModule";
2
- import { initExampleModule } from "./modules/exampleModule"
1
+ // import { initExampleModule } from "./modules/exampleModule"
3
2
 
4
3
  function initGlobal() {
5
4
  // initExampleModule();
@@ -26,14 +26,6 @@
26
26
  "renameFailed": "[error] could not rename page files: {error}",
27
27
  "deleteFailed": "[error] {path}: {error}"
28
28
  },
29
- "components": {
30
- "fileMissing": "[skip] not found: {path}",
31
- "anchorMissing": "[skip] no {anchor} anchor in {file}",
32
- "blockAdded": "[updated] page block added for \"{name}\"",
33
- "blockMissing": "[skip] page block for \"{name}\" not found",
34
- "blockRemoved": "[cleaned] page block removed for \"{name}\"",
35
- "blockRenamed": "[renamed] page block \"{source}\" \u2192 \"{destination}\""
36
- },
37
29
  "pagesData": {
38
30
  "fileMissing": "[skip] not found: {path}",
39
31
  "invalidStructure": "[error] {file} must contain an object of pages",
@@ -13,7 +13,6 @@
13
13
  "stylePages": "src/frontend/scss/pages",
14
14
  "pagesData": "src/frontend/data/pages.json",
15
15
  "siteData": "src/frontend/data/site.json",
16
- "pageComponents": "src/frontend/layouts/page-components.njk",
17
16
  "templates": "tools/res/templates"
18
17
  },
19
18
  "languages": {
@@ -94,11 +93,6 @@
94
93
  }
95
94
  }
96
95
  },
97
- "pageComponents": {
98
- "anchor": "{% else %}",
99
- "condition": "{% elif title == \"{camelName}\" %}",
100
- "include": "{% include \"component-name.njk\" ignore missing %}"
101
- },
102
96
  "frontMatter": {
103
97
  "title": "title: \"{camelName}\"",
104
98
  "permalink": "permalink: \"/{pageName}/\""
@@ -7,7 +7,6 @@ const { formatText } = require('./text');
7
7
  const { languageSettings } = require('./project');
8
8
  const { getPageArtifacts, pageExists } = require('./pageArtifacts');
9
9
  const { readPagesData, addPageData, removePageData, renamePageData } = require('./pagesData');
10
- const { addPageBlock, removePageBlock, renamePageBlock } = require('./pageComponents');
11
10
 
12
11
  const FRONT_MATTER_PATTERNS = Object.freeze({
13
12
  title: /^title:.*$/m,
@@ -62,7 +61,6 @@ function addPage(pageName) {
62
61
  return;
63
62
  }
64
63
 
65
- addPageBlock(pageName);
66
64
  addPageData(pageName);
67
65
  }
68
66
 
@@ -104,7 +102,6 @@ function renamePage(oldName, newName) {
104
102
  return;
105
103
  }
106
104
 
107
- renamePageBlock(oldName, newName);
108
105
  renamePageData(oldName, newName);
109
106
  }
110
107
 
@@ -134,7 +131,6 @@ function removePage(pageName) {
134
131
  }
135
132
  }
136
133
 
137
- removePageBlock(pageName);
138
134
  removePageData(pageName);
139
135
 
140
136
  const sourceDirectories = [source.style, source.script, source.route].map(file => path.dirname(file));
@@ -46,7 +46,6 @@ const PATHS = Object.freeze({
46
46
  get stylePages() { return resolveFromProject(settings.paths.stylePages); },
47
47
  get siteData() { return resolveFromProject(settings.paths.siteData); },
48
48
  get pagesData() { return resolveFromProject(settings.paths.pagesData); },
49
- get pageComponents() { return resolveFromProject(settings.paths.pageComponents); },
50
49
  get templates() { return resolveFromPackage(settings.paths.templates); },
51
50
  });
52
51
 
@@ -13,7 +13,6 @@ function requiredFiles() {
13
13
  { label: settings.paths.packageJson, path: PATHS.packageJson },
14
14
  { label: settings.paths.siteData, path: PATHS.siteData },
15
15
  { label: settings.paths.pagesData, path: PATHS.pagesData },
16
- { label: settings.paths.pageComponents, path: PATHS.pageComponents },
17
16
  { label: message('validation.templatesLabel'), path: PATHS.templates },
18
17
  ];
19
18
  }
@@ -1,9 +1,10 @@
1
1
  ---
2
2
  title: ""
3
3
  permalink: ""
4
- layout: page-components.njk
4
+ layout: base.njk
5
5
  ---
6
6
 
7
7
  <!-- !IMPORTANT -->
8
- <!-- DO NOT ADD ANYTHING HERE -->
9
- <!-- You should create a new component.njk into src/frontend and include that in layouts/page-components.njk -->
8
+ <!-- To keep order, the best practice is to create a new component.njk file in components/ and include it here-->
9
+
10
+ {% include "component.njk" ignore missing %}
@@ -1,17 +0,0 @@
1
- ---
2
- title: "404"
3
- permalink: 404.html
4
- layout: base.njk
5
- ---
6
-
7
- <!-- !IMPORTANT -->
8
- <!-- This is the only page that you need to modify statically -->
9
-
10
- <div class="fade-in not-found">
11
- <h1>Oops! Page not found</h1>
12
- <a href="/">Return to homepage</a>
13
- </div>
14
-
15
-
16
- {# You can also add the includes you need here below
17
- {% include "component.njk" %} #}
@@ -1,9 +0,0 @@
1
- ---
2
- title: homepage
3
- permalink: /
4
- layout: page-components.njk
5
- ---
6
-
7
- <!-- !IMPORTANT -->
8
- <!-- DO NOT ADD ANYTHING HERE -->
9
- <!-- You should create a new component.njk into src/components and include that in layouts/page-components.njk -->
@@ -1,14 +0,0 @@
1
- ---
2
- layout: base.njk
3
- ---
4
-
5
- <!-- NJK (HTML) components per page -->
6
-
7
- {% if title == "homepage" %}
8
- {% include "welcome.njk" ignore missing %}
9
-
10
- {% else %}
11
- <!-- You should not touch this else statement -->
12
- {% include "404/_404.njk" %}
13
- {{ content | safe }}
14
- {% endif %}
@@ -1,88 +0,0 @@
1
- const path = require('path');
2
- const settings = require('../config/settings.json');
3
- const { PATHS } = require('./paths');
4
- const { exists, readText, writeText } = require('./files');
5
- const { log } = require('./logger');
6
- const { toCamelCase, formatText } = require('./text');
7
-
8
- const EXTRA_BLANK_LINES_PATTERN = /\n\s*\n\s*\n/g;
9
- const BLANK_LINES = '\n\n';
10
- const ANCHOR = settings.pageComponents.anchor;
11
-
12
- function blockPattern(camelName) {
13
- return new RegExp(
14
- `[ \\t]*\\{%\\s*elif\\s+title\\s*==\\s*"${camelName}"\\s*%\\}[\\s\\S]*?(?=[ \\t]*\\{%\\s*(?:elif|else|endif))`
15
- );
16
- }
17
-
18
- function conditionFor(camelName) {
19
- return formatText(settings.pageComponents.condition, { camelName });
20
- }
21
-
22
- function readPageComponents() {
23
- if (!exists(PATHS.pageComponents)) {
24
- log('components.fileMissing', { path: PATHS.pageComponents });
25
- return null;
26
- }
27
- return readText(PATHS.pageComponents);
28
- }
29
-
30
- function writePageComponents(content) {
31
- writeText(PATHS.pageComponents, content);
32
- }
33
-
34
- function addPageBlock(pageName) {
35
- const content = readPageComponents();
36
- if (content === null) return;
37
-
38
- const camelName = toCamelCase(pageName);
39
- const condition = conditionFor(camelName);
40
- if (content.includes(condition)) return;
41
-
42
- if (!content.includes(ANCHOR)) {
43
- log('components.anchorMissing', { anchor: ANCHOR, file: path.basename(PATHS.pageComponents) });
44
- return;
45
- }
46
-
47
- const block = `${condition}\n${settings.pageComponents.include}\n\n`;
48
-
49
- writePageComponents(content.replace(ANCHOR, `${block}${ANCHOR}`));
50
- log('components.blockAdded', { name: camelName });
51
- }
52
-
53
- function removePageBlock(pageName) {
54
- const content = readPageComponents();
55
- if (content === null) return;
56
-
57
- const camelName = toCamelCase(pageName);
58
- if (!blockPattern(camelName).test(content)) {
59
- log('components.blockMissing', { name: camelName });
60
- return;
61
- }
62
-
63
- const updated = content
64
- .replace(blockPattern(camelName), '')
65
- .replace(EXTRA_BLANK_LINES_PATTERN, BLANK_LINES);
66
-
67
- writePageComponents(updated);
68
- log('components.blockRemoved', { name: camelName });
69
- }
70
-
71
- function renamePageBlock(oldName, newName) {
72
- const content = readPageComponents();
73
- if (content === null) return;
74
-
75
- const oldCamelName = toCamelCase(oldName);
76
- const newCamelName = toCamelCase(newName);
77
- const oldCondition = conditionFor(oldCamelName);
78
-
79
- if (!content.includes(oldCondition)) {
80
- log('components.blockMissing', { name: oldCamelName });
81
- return;
82
- }
83
-
84
- writePageComponents(content.replace(oldCondition, conditionFor(newCamelName)));
85
- log('components.blockRenamed', { source: oldCamelName, destination: newCamelName });
86
- }
87
-
88
- module.exports = { addPageBlock, removePageBlock, renamePageBlock };
File without changes
File without changes
File without changes