nibula 1.5.0 → 1.5.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/.eleventyignore CHANGED
@@ -1,4 +1 @@
1
- src/frontend/assets/**
2
- src/frontend/js/**
3
- src/frontend/scss/**
4
- src/backend/**
1
+ src/frontend/assets/**
package/CHANGELOG.md CHANGED
@@ -5,6 +5,49 @@ 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.5.1] - 2026-08-05
9
+
10
+ ### Fixed
11
+ - `nib new` no longer creates an empty `routes` folder. Page source folders are
12
+ created on demand when a page is added, and removed again when the last page
13
+ in them is deleted — `routes`, `scss/pages` and the JS/TS pages folder alike.
14
+ - Removing a page that doesn't exist reported the skip and then went on to strip
15
+ its entry from `site.json` and `page-components.njk` anyway. It now stops.
16
+ - `example.config.php` was missing the `APP_ENV` key, so PHP projects had no way
17
+ to discover the debug switch from the template. Behaviour was unaffected —
18
+ the code already fell back to `production` — but the key is now visible and
19
+ documented, and the two templates are identical again.
20
+ - The backend's `dev` script set `APP_ENV` as an environment variable, which
21
+ neither backend reads. It ran in production mode and produced no debug output.
22
+ The script is gone; error verbosity is set in `config.js` / `config.php`.
23
+ - `bin/create.js` repeated the `mysql2` version already declared in
24
+ `src/backend/package.json`. It now reads that file, so the two cannot drift.
25
+ - `docs/Deploy.md` pointed at `backend/backend-node.service.example`, removed in
26
+ 1.5.0. The systemd unit is written out in full instead.
27
+ - `docs/Deploy.md` gave the backend path as `/var/www/backend/SITE_FOLDER`,
28
+ inverted with respect to the rest of the document.
29
+
30
+ ### Changed
31
+ - Nginx setup steps moved from the `nginx.conf` header into `docs/Deploy.md`,
32
+ under "Your own server". The header keeps only the three placeholders that
33
+ appear in the file and a pointer to the docs, so a config copied into
34
+ `/etc/nginx/sites-available/` no longer carries 60 lines of install
35
+ instructions that were already followed.
36
+ - `docs/Deploy.md` troubleshooting is now a table, with a row for a 502 on
37
+ `/api` caused by the Node process not running.
38
+ - `.eleventyignore` reduced to `src/frontend/assets/**`. The `scss`, `js` and
39
+ `src/backend` entries excluded nothing: the first two are not Eleventy
40
+ template formats and the third is outside the input directory. The last one
41
+ was also misleading, since `.eleventy.js` deliberately copies `src/backend`
42
+ into the build.
43
+ - `docs/Backend.md` documents `APP_ENV` in its own section, including the
44
+ `display_errors` behaviour specific to PHP, and states explicitly that it is
45
+ a config key rather than an environment variable.
46
+
47
+ ### Notes
48
+ - Existing projects are unaffected: the scaffolding and template changes apply
49
+ only to newly created ones.
50
+
8
51
  ## [1.5.0] - 2026-08-04
9
52
 
10
53
  ### Added
package/README.md CHANGED
@@ -76,19 +76,6 @@ cd your-project
76
76
  nib run
77
77
  ```
78
78
 
79
- ## 💻 Commands
80
-
81
- Run these from anywhere inside a project — except `nib new`, which must be run **outside** a project, in the folder where you want the new one created.
82
-
83
- | Command | Description |
84
- |---|---|
85
- | `nib new <name>` | Create your new project |
86
- | `nib run` | Start the dev server and build the output folder at runtime |
87
- | `nib cli` | Open the page-management assistant |
88
- | `nib build` | Build the output folder |
89
- | `nib clean` | Remove the output directory |
90
- | `nib update` | Update Nibula to the latest version (applies from your next project on) |
91
-
92
79
  ## 📄 Managing pages
93
80
 
94
81
  Instead of creating and wiring up multiple files by hand, let the interactive assistant do it for you. To create, remove, or rename pages and configure the output path, run:
@@ -99,17 +86,18 @@ nib cli
99
86
 
100
87
  See [docs/Assistant CLI.md](docs/Assistant%20CLI.md) for details.
101
88
 
102
- ## 🌍 Deploying
103
-
104
- Nibula builds a static site into your `out` folder, which you upload to a web
105
- server. Which backend you chose affects deployment:
89
+ ## 💻 Commands
106
90
 
107
- - **Node** runs as a long-running service on a VPS; the web server reverse-proxies `/api` to it.
108
- - **PHP** runs on ordinary shared hosting (Apache/IIS) or a VPS with PHP-FPM — no process to keep alive.
91
+ Run these from anywhere inside a project — except `nib new`, which must be run **outside** a project, in the folder where you want the new one created.
109
92
 
110
- The shipped `.htaccess`, `web.config` and `nginx.conf` cover **both** backends.
111
- See [docs/Deploy.md](docs/Deploy.md) for the full guide, including how to start
112
- the Node service on your server.
93
+ | Command | Description |
94
+ |---|---|
95
+ | `nib new <name>` | Create your new project |
96
+ | `nib run` | Start the dev server and build the output folder at runtime |
97
+ | `nib cli` | Open the page-management assistant |
98
+ | `nib build` | Build the output folder |
99
+ | `nib clean` | Remove the output directory |
100
+ | `nib update` | Update Nibula to the latest version (applies from your next project on) |
113
101
 
114
102
  ## 🙌 Wrapping up
115
103
 
@@ -117,4 +105,4 @@ Thanks for giving Nibula a try.
117
105
 
118
106
  It's a one-person project, so every bit of feedback counts more than you'd think: if something breaks, if the documentation isn't clear, or if a feature is missing, open an [issue](https://github.com/Rhaastrake/Nibula/issues) — even just to tell me what you built with it.
119
107
 
120
- If Nibula was useful to you, leaving a ⭐ on the repository is the easiest way to help other people find it.
108
+ If Nibula was useful to you, leaving a ⭐ on the repository is the easiest way to help other people find it.
package/bin/create.js CHANGED
@@ -53,11 +53,11 @@ const BACKEND_CHOICES = [
53
53
  { label: 'PHP (Can run everywhere)', value: BACKEND.PHP },
54
54
  ];
55
55
 
56
- // Runtime dependency added to the ROOT package.json when the Node backend is
57
- // chosen, so it lands in the root node_modules (never in src/backend).
58
- const NODE_BACKEND_DEPENDENCIES = {
59
- mysql2: '^3.11.0',
60
- };
56
+ // Runtime dependencies for the Node backend, read from the backend's own
57
+ // package.json so the two never drift. They are added to the ROOT package.json
58
+ // when Node is chosen, so they land in the root node_modules (never in
59
+ // src/backend).
60
+ const NODE_BACKEND_DEPENDENCIES = require(path.join(templateDir, 'src/backend/package.json')).dependencies;
61
61
 
62
62
  // ── COPY CONFIG ───────────────────────────────────────────────────────────────
63
63
 
package/docs/Backend.md CHANGED
@@ -107,19 +107,32 @@ both, PHP-array vs JS-object syntax.
107
107
 
108
108
  ```js
109
109
  module.exports = {
110
+ // Default key for protected endpoints that don't have a specific key in CUSTOM_ENDPOINT_KEYS
110
111
  GENERAL_API_KEY: 'DEFAULT_KEY',
112
+
113
+ // If you want to restrict access to protected endpoints to specific clients, define custom keys per endpoint.
114
+ // For subfolder endpoints, use the relative path ('subfolder/endpoint')
111
115
  CUSTOM_ENDPOINT_KEYS: {
112
- 'subfolder/endpoint': 'custom-key',
116
+ 'subfolder/example-protected': 'custom-key',
113
117
  },
114
- GENERAL_ALLOWED_ORIGINS: ['*' /*, 'https://example.com' */],
118
+
119
+ GENERAL_ALLOWED_ORIGINS: [
120
+ '*',
121
+ // 'https://example.com',
122
+ ],
123
+
115
124
  CUSTOM_ENDPOINT_ORIGINS: {
116
- 'subfolder/endpoint': ['https://app.example.com'],
125
+ 'subfolder/example-protected': ['https://app.example.com'],
117
126
  },
127
+
128
+ // Database configuration
118
129
  DB_HOST: '127.0.0.1',
119
130
  DB_NAME: 'example_db',
120
131
  DB_USER: 'root',
121
132
  DB_PASS: '',
122
- APP_ENV: 'production', // anything else = verbose errors
133
+
134
+ // Environment: 'production' hides error details; anything else = debug.
135
+ APP_ENV: 'production',
123
136
  };
124
137
  ```
125
138
 
@@ -147,6 +160,26 @@ endpoints).
147
160
  Resolution order: `CUSTOM_ENDPOINT_ORIGINS[path]` if present, otherwise
148
161
  `GENERAL_ALLOWED_ORIGINS`.
149
162
 
163
+ ### Error verbosity
164
+
165
+ `APP_ENV` decides what an unhandled exception tells the client.
166
+
167
+ | Value | Response to a 500 |
168
+ |---|---|
169
+ | `'production'` (default) | `{"status":"error","message":"Internal server error","code":500}` |
170
+ | anything else | the real exception message, plus `stack` (Node) or `file` and `line` (PHP) |
171
+
172
+ On PHP it does one extra thing: `'production'` also turns off `display_errors`
173
+ and `error_reporting`, so a stray warning can't be printed before the JSON and
174
+ corrupt the response body.
175
+
176
+ Set it to something like `'development'` while building, and back to
177
+ `'production'` before you publish. Missing key means `'production'`.
178
+
179
+ > This is a config key, **not** an environment variable. Setting `APP_ENV` in a
180
+ > shell or in a systemd unit has no effect — both backends read it from
181
+ > `config.js` / `config.php` only.
182
+
150
183
  ## Database
151
184
 
152
185
  Both backends expose a connection singleton reading the same config —
package/docs/Deploy.md CHANGED
@@ -62,6 +62,8 @@ automatically.
62
62
  modules) proxies `/api` to Node and wins if present. Remove it to fall through
63
63
  to `ApiToPhp`, which rewrites `/api` to the PHP front controller.
64
64
 
65
+ ---
66
+
65
67
  ## Your own server (Nginx on a VPS)
66
68
 
67
69
  Nginx doesn't read per-directory config files — the whole site lives in one file.
@@ -69,6 +71,68 @@ Nibula ships `nginx.conf` in the project root as a ready-to-use site file that
69
71
  **covers both backends without edits**: `/api` goes to Node, and if Node is
70
72
  unreachable the request falls through to PHP automatically.
71
73
 
74
+ Upload the contents of `out` to `/var/www/SITE_FOLDER`, then follow the steps
75
+ below.
76
+
77
+ ### 1. Replace
78
+
79
+ | Placeholder | Where |
80
+ |---|---|
81
+ | `YOUR_DOMAIN` | `server_name`, twice (port 80 and 443) |
82
+ | `SITE_FOLDER` | document root under `/var/www/` |
83
+ | `YOUR_CERTIFICATE` | dir under `/etc/letsencrypt/live/` |
84
+ | `SITE_NAME` | file name in the commands below |
85
+
86
+ ### 2. Install (skip if already present)
87
+
88
+ ```bash
89
+ sudo apt update
90
+ sudo apt install -y nginx certbot python3-certbot-nginx
91
+ ```
92
+
93
+ If you are using PHP as backend:
94
+
95
+ ```bash
96
+ sudo apt install -y php-fpm
97
+ ```
98
+
99
+ ### 3. Certificate
100
+
101
+ ```bash
102
+ sudo certbot certonly --nginx --cert-name SITE_NAME -d YOUR_DOMAIN
103
+ sudo systemctl enable --now certbot.timer # auto-renew every 90 days
104
+ ```
105
+
106
+ ### 4. Install the site
107
+
108
+ ```bash
109
+ # Move to nginx folders
110
+ cd /etc/nginx/sites-available
111
+
112
+ # Create and write a new file
113
+ sudo nano SITE_NAME
114
+ # CTRL + O, ENTER, CTRL + X
115
+
116
+ # Copy the config to sites-enabled
117
+ sudo cp SITE_NAME ../sites-enabled/
118
+
119
+ # Test and reload the nginx configuration
120
+ sudo nginx -t && sudo systemctl reload nginx
121
+ ```
122
+
123
+ ### If it fails
124
+
125
+ | Symptom | Cause |
126
+ |---|---|
127
+ | `cannot load certificate ... Permission denied` | `nginx -t` needs sudo |
128
+ | `cannot load certificate ... No such file` | wrong certificate name |
129
+ | `no alternative certificate subject name` | certificate doesn't cover domain |
130
+ | `unknown directive "http2"` | nginx < 1.25, check with `nginx -v` |
131
+ | NXDOMAIN / timeout during certbot | DNS not pointing here |
132
+ | 500 instead of the 404 page | `404.html` missing |
133
+ | 502 on `/api`, Node deployed | Node process not running |
134
+ | 502 on `/api`, PHP deployed | wrong php-fpm socket path |
135
+
72
136
  ---
73
137
 
74
138
  ## Starting the Node backend on the server
@@ -77,7 +141,7 @@ After uploading `out`:
77
141
 
78
142
  1. Install the backend's runtime deps:
79
143
  ```bash
80
- cd /var/www/backend/SITE_FOLDER
144
+ cd /var/www/SITE_FOLDER/backend
81
145
  npm install
82
146
  ```
83
147
 
@@ -91,24 +155,50 @@ After uploading `out`:
91
155
  sudo node /var/www/SITE_FOLDER/backend/_core/index.js
92
156
  curl http://127.0.0.1:3000/api/example-public
93
157
  ```
94
- 4. Exit the screen without closing the process with `CTRL + A`, `D`. To resume the screen type `screen -r node-backend`.
95
158
 
96
- 5. For production use **systemd**, which also restarts on crash and reboot. An
97
- example unit ships at `backend/backend-node.service.example` — copy it to
98
- `/etc/systemd/system/backend-node.service`, adjust `WorkingDirectory` and the
99
- `node` path (`which node`), then:
159
+ 4. Exit the screen without closing the process with `CTRL + A`, `D`. To resume
160
+ the screen type `screen -r node-backend`.
161
+
162
+ 5. For production use **systemd**, which also restarts on crash and reboot.
163
+ Create `/etc/systemd/system/backend-node.service`:
164
+
165
+ ```ini
166
+ [Unit]
167
+ Description=Nibula Node backend
168
+ After=network.target
169
+
170
+ [Service]
171
+ Type=simple
172
+ User=www-data
173
+ WorkingDirectory=/var/www/SITE_FOLDER/backend
174
+ ExecStart=/usr/bin/node /var/www/SITE_FOLDER/backend/_core/index.js
175
+ Restart=always
176
+ RestartSec=5
177
+
178
+ [Install]
179
+ WantedBy=multi-user.target
180
+ ```
181
+
182
+ Adjust `WorkingDirectory` and the `node` path (`which node`), then:
183
+
100
184
  ```bash
101
185
  sudo systemctl daemon-reload
102
186
  sudo systemctl enable --now backend-node
103
187
  sudo systemctl status backend-node
104
188
  ```
189
+
105
190
  (`pm2` works too: `pm2 start _core/index.js --name backend-node`.)
106
191
 
107
192
  ### Environment variables
108
193
 
194
+ Read by the Node backend at startup. Set them in the systemd unit with
195
+ `Environment=KEY=value`.
196
+
109
197
  | Variable | Default | Purpose |
110
198
  |---|---|---|
111
- | `PORT` | `3000` | Port Node listens on (match your proxy target) |
199
+ | `PORT` | `3000` | Port Node listens on (must match the proxy target in `nginx.conf`) |
112
200
  | `HOST` | `127.0.0.1` | Bind address — keep it local; the web server is the public face |
113
- | `APP_ENV` | `production` | `production` hides error details; anything else = verbose 500s |
114
- | `DOCUMENT_ROOT` | auto (`SITE_FOLDER/`) | Where `404.html` lives; detected as the folder above `backend/` |
201
+ | `DOCUMENT_ROOT` | auto (`SITE_FOLDER/`) | Where `404.html` lives; detected as the folder above `backend/` |
202
+
203
+ Error verbosity is **not** an environment variable: it is the `APP_ENV` key in
204
+ `config.js` / `config.php`. See `docs/Backend.md`.
package/nginx.conf CHANGED
@@ -1,49 +1,12 @@
1
1
  # =============================================================================
2
- # Nibula — default nginx site configuration
2
+ # Nibula — nginx site configuration
3
3
  # -----------------------------------------------------------------------------
4
- # 1. REPLACE
4
+ # Replace the three placeholders below, then follow the setup steps in
5
+ # docs/Deploy.md → "Your own server (Nginx on a VPS)".
5
6
  #
6
- # YOUR_DOMAIN server_name, twice (port 80 and 443)
7
- # SITE_FOLDER document root under /var/www/
8
- # YOUR_CERTIFICATE dir under /etc/letsencrypt/live/
9
- # SITE_NAME commands below file name
10
- #
11
- # 2. INSTALL (skip if already present)
12
- #
13
- # sudo apt update
14
- # sudo apt install -y nginx certbot python3-certbot-nginx
15
- #
16
- # If you are using PHP as backend:
17
- # sudo apt install -y php-fpm
18
- #
19
- # 3. CERTIFICATE
20
- #
21
- # sudo certbot certonly --nginx --cert-name SITE_NAME -d YOUR_DOMAIN
22
- # sudo systemctl enable --now certbot.timer # auto-renew every 90 days
23
- #
24
- # 4. INSTALL THE SITE
25
- #
26
- # Move to nginx folders
27
- # cd /etc/nginx/sites-available
28
- #
29
- # Create and write a new file
30
- # sudo nano SITE_NAME
31
- # CTRL + O, ENTER, CTRL + X
32
- #
33
- # Copy the config to sites-enabled
34
- # sudo cp SITE_NAME ../sites-enabled/
35
- #
36
- # Test and reload the nginx configuration
37
- # sudo nginx -t && sudo systemctl reload nginx
38
- #
39
- # IF IT FAILS
40
- # cannot load certificate ... Permission denied → nginx -t needs sudo
41
- # cannot load certificate ... No such file → wrong certificate name
42
- # no alternative certificate subject name → certificate doesn't cover domain
43
- # unknown directive "http2" → nginx < 1.25, check with nginx -v
44
- # 500 instead of the 404 page → 404.html missing
45
- # NXDOMAIN / timeout during certbot → DNS not pointing here
46
- # 502 on /api with PHP deployed → wrong php-fpm socket path
7
+ # YOUR_DOMAIN your domain — server_name, twice (port 80 and 443)
8
+ # SITE_FOLDER the folder under /var/www/ holding the site
9
+ # YOUR_CERTIFICATE the directory name under /etc/letsencrypt/live/
47
10
  # =============================================================================
48
11
 
49
12
  # HTTP (80) configuration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nibula",
3
- "version": "1.5.0",
3
+ "version": "1.5.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
  "static-site-generator",
@@ -1,14 +1,21 @@
1
1
  <?php
2
2
  declare(strict_types=1);
3
3
 
4
+ /**
5
+ * Mirror of src/backend/example.config.js
6
+ *
7
+ * Versioned, secret-free template. On setup, copy this file to config.php and
8
+ * fill in real values. config.php is git-ignored and stays local.
9
+ */
10
+
4
11
  return [
5
12
  // Default key for protected endpoints that don't have a specific key in CUSTOM_ENDPOINT_KEYS
6
13
  'GENERAL_API_KEY' => 'DEFAULT_KEY',
7
14
 
8
- // If you want restrict access to protected endpoints to specific clients, you can define custom keys for each endpoint
15
+ // If you want to restrict access to protected endpoints to specific clients, define custom keys per endpoint.
9
16
  // For subfolder endpoints, use the relative path ('subfolder/endpoint')
10
17
  'CUSTOM_ENDPOINT_KEYS' => [
11
- 'subfolder/example-protected' => 'custom-key',
18
+ 'subfolder/example-protected' => 'custom-key',
12
19
  ],
13
20
 
14
21
  'GENERAL_ALLOWED_ORIGINS' => [
@@ -17,7 +24,7 @@ return [
17
24
  ],
18
25
 
19
26
  'CUSTOM_ENDPOINT_ORIGINS' => [
20
- 'subfolder/example-protected' => ['https://app.example.com'],
27
+ 'subfolder/example-protected' => ['https://app.example.com'],
21
28
  ],
22
29
 
23
30
  // Database configuration
@@ -25,4 +32,7 @@ return [
25
32
  'DB_NAME' => 'example_db',
26
33
  'DB_USER' => 'root',
27
34
  'DB_PASS' => '',
35
+
36
+ // Environment: 'production' hides error details; anything else = debug.
37
+ 'APP_ENV' => 'production',
28
38
  ];
@@ -6,8 +6,7 @@
6
6
  "main": "_core/index.js",
7
7
  "type": "commonjs",
8
8
  "scripts": {
9
- "start": "node _core/index.js",
10
- "dev": "APP_ENV=development node _core/index.js"
9
+ "start": "node _core/index.js"
11
10
  },
12
11
  "engines": {
13
12
  "node": ">=18"
@@ -52,6 +52,16 @@ function removeDirectory(target) {
52
52
  return true;
53
53
  }
54
54
 
55
+ function removeDirectoryIfEmpty(target) {
56
+ try {
57
+ if (fs.readdirSync(target).length > 0) return false;
58
+ fs.rmdirSync(target);
59
+ return true;
60
+ } catch (error) {
61
+ return false;
62
+ }
63
+ }
64
+
55
65
  module.exports = {
56
66
  exists,
57
67
  readText,
@@ -62,4 +72,5 @@ module.exports = {
62
72
  moveFile,
63
73
  removeFile,
64
74
  removeDirectory,
65
- };
75
+ removeDirectoryIfEmpty,
76
+ };
@@ -1,7 +1,7 @@
1
1
  const path = require('path');
2
2
  const settings = require('../config/settings.json');
3
3
  const { PATHS } = require('./paths');
4
- const { exists, readText, writeText, copyFile, moveFile, removeFile, removeDirectory } = require('./files');
4
+ const { exists, readText, writeText, copyFile, moveFile, removeFile, removeDirectory, removeDirectoryIfEmpty } = require('./files');
5
5
  const { log } = require('./logger');
6
6
  const { formatText } = require('./text');
7
7
  const { languageSettings } = require('./project');
@@ -133,6 +133,12 @@ function removePage(pageName) {
133
133
 
134
134
  removePageBlock(pageName);
135
135
  removeSiteData(pageName);
136
+
137
+ const sourceDirectories = [source.style, source.script, source.route].map(file => path.dirname(file));
138
+
139
+ for (const directory of sourceDirectories) {
140
+ if (removeDirectoryIfEmpty(directory)) log('page.fileDeleted', { path: directory });
141
+ }
136
142
  }
137
143
 
138
144
  module.exports = { addPage, removePage, renamePage };
@@ -1,37 +0,0 @@
1
- name: Publish to npm
2
-
3
- on:
4
- release:
5
- types: [published]
6
- workflow_dispatch:
7
-
8
- permissions:
9
- contents: read
10
- id-token: write
11
-
12
- jobs:
13
- publish:
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: actions/checkout@v4
17
-
18
- - uses: actions/setup-node@v4
19
- with:
20
- node-version: 22
21
- registry-url: https://registry.npmjs.org
22
-
23
- - name: Check that package.json matches the tag
24
- if: github.event_name == 'release'
25
- run: |
26
- TAG="${GITHUB_REF_NAME#v}"
27
- PKG=$(node -p "require('./package.json').version")
28
- if [ "$TAG" != "$PKG" ]; then
29
- echo "Tag $TAG does not match package.json version $PKG"
30
- exit 1
31
- fi
32
-
33
- - run: npm install -g npm@latest
34
-
35
- - run: npm ci
36
-
37
- - run: npm publish --provenance --access public