nibula 1.4.1 → 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,21 +5,78 @@ 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.4.1] - 2026-08-03
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
+
51
+ ## [1.5.0] - 2026-08-04
9
52
 
10
53
  ### Added
11
- - `nginx.conf` header now documents the full setup: package install,
12
- certificate issuance with `certbot --cert-name`, auto-renew, and the
13
- `sites-available` → `sites-enabled` steps.
14
- - Troubleshooting table covering the errors whose message points elsewhere
15
- than the actual cause: `nginx -t` without sudo, wrong certificate name,
16
- certificate not covering the domain, nginx < 1.25, missing `404.html`,
17
- DNS not pointing to the server.
54
+ - `nginx.conf` now covers **both backends** with no manual edits. `/api` is
55
+ proxied to Node on `127.0.0.1:3000`; if Node is unreachable, the request is
56
+ retried through the PHP front controller via the internal `@php_backend`
57
+ location. The same file works for a Node deployment and a PHP one.
58
+ - `nginx.conf` header now documents the PHP-FPM requirement, the version-agnostic
59
+ socket, and the errors specific to a PHP deployment.
18
60
 
19
61
  ### Changed
20
- - Certificates are now issued with `--cert-name SITE_NAME`, so the directory
21
- under `/etc/letsencrypt/live/` matches the site name and `YOUR_CERTIFICATE`
22
- is easier to fill in.
62
+ - PHP is invoked through `SCRIPT_FILENAME` instead of by URL, so `.php` requests
63
+ keep returning 404 even on a PHP deployment and no source file is ever
64
+ reachable by name.
65
+ - `fastcgi_pass` targets the version-agnostic socket `unix:/run/php/php-fpm.sock`
66
+ instead of a versioned path, so the config survives PHP upgrades.
67
+ - `web.config` is now blocked anywhere in the tree, not only at the document root.
68
+ - `docs/Backend.md` is limited to writing endpoints: routing, `public` vs
69
+ `protected`, the `Response` helper and configuration. Examples are Node-only,
70
+ with a note that PHP behaves identically. Hosting and server requirements
71
+ moved to `docs/Deploy.md`.
72
+ - `docs/Deploy.md` rewritten around where a site can be published and what each
73
+ target requires, including why shared hosting cannot run the Node backend.
74
+
75
+ ### Removed
76
+ - `docs/Nginx.md` — the setup steps live in the `nginx.conf` header, where they
77
+ are read at the moment they are needed.
78
+ - `src/backend/backend-node.service.example` — the systemd unit is documented
79
+ in `docs/Deploy.md` instead of shipped as a file.
23
80
 
24
81
  ## [1.4.0] - 2026-08-03
25
82
 
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
@@ -1,46 +1,24 @@
1
1
  # Backend
2
2
 
3
- Nibula ships **two interchangeable backends** in `src/backend/` — a **PHP** one
4
- and a **Node.js** one. You pick which to use when you scaffold a project
5
- (`nib new`). Both provide the same REST API: same routing, same `Response`
6
- envelope, same `X-Api-Key` auth, same CORS model, same file-based rate limiter.
7
- Only the language differs.
8
-
9
- > **Server requirement** — where each backend can run:
10
- >
11
- > | Host | PHP | Node.js |
12
- > |---|---|---|
13
- > | Apache (shared hosting, VPS) | ✅ | ⚠️ needs a persistent process (VPS only) |
14
- > | Nginx (VPS) | ⚠️ works, but the shipped `nginx.conf` targets Node — needs a manual `fastcgi_pass` block | ✅ (Nginx reverse-proxies to Node) |
15
- > | IIS (Windows Server) | ✅ FastCGI | ✅ via ARR reverse proxy |
16
- > | Static hosting (Netlify, Vercel, GitHub Pages, Cloudflare Pages) | ❌ | ❌ |
17
- >
18
- > Typical shared hosting can run **PHP** but not a long-running Node process, so
19
- > choose PHP there. On a VPS you control, either works.
20
-
21
- The backend lives in `src/backend/` and is copied to the output directory
22
- automatically at build time.
23
-
24
- ## Choosing a backend
25
-
26
- When you run `nib new`, the scaffolder asks three questions: language, CSS
27
- framework, and **backend**. Pick Node.js or PHP.
28
-
29
- - **If you pick Node**, Composer is never run — you get an npm-only setup, and
30
- the backend's dependency (`mysql2`, used only if you touch the database) is
31
- added to the **root** `package.json` and installed into the root `node_modules`.
32
- - **If you pick PHP**, Composer dependencies are installed as before, and no Node
33
- backend files are added.
34
-
35
- **Only the backend you choose is copied into the project.** A Node project
36
- contains no PHP files; a PHP project contains no `.js` backend files. Shared,
3
+ Nibula ships **two interchangeable backends** — PHP and Node.js. You pick one
4
+ when you scaffold a project with `nib new`, and **only that one is copied into
5
+ your project**: a Node project contains no PHP files and vice versa. Shared,
37
6
  backend-agnostic files (SQL migrations, `.htaccess`, `web.config`) are always
38
7
  included.
39
8
 
9
+ Both provide the same REST API: same routing, same `Response` envelope, same
10
+ `X-Api-Key` auth, same CORS model, same rate limiter. Only the syntax differs.
11
+
12
+ > **The examples below are in JavaScript (the Node backend).** If you chose PHP,
13
+ > everything works exactly the same way — same routing, same variables, same
14
+ > `Response` helper, same behaviour. Only the syntax differs.
15
+
16
+ For **where each backend can run** and how to deploy it, see `docs/Deploy.md`.
17
+
40
18
  ## Structure
41
19
 
42
- The structure below shows both backends for reference; **your project will have
43
- only one side of each pair** (plus the shared files).
20
+ The tree below shows both backends for reference; your project will have only
21
+ one side of each pair.
44
22
 
45
23
  ```
46
24
  src/backend/
@@ -48,152 +26,56 @@ src/backend/
48
26
  │ ├── public/ # Endpoints accessible without an API key
49
27
  │ └── protected/ # Endpoints requiring the X-Api-Key header
50
28
  ├── _core/ # Framework internals (routing, modules) — do not edit
51
- │ ├── index.php # PHP front controller (PHP project)
52
- │ ├── index.js # Node front controller + server (Node project)
29
+ │ ├── index.php # PHP front controller (PHP project)
30
+ │ ├── index.js # Node front controller + server (Node project)
53
31
  │ ├── init.php / init.js
54
32
  │ └── modules/ # Response, RateLimiter
55
33
  ├── database/
56
- │ ├── Database.php # PDO singleton (PHP project)
57
- │ ├── Database.js # mysql2 pool singleton (Node project)
34
+ │ ├── Database.php # PDO singleton (PHP project)
35
+ │ ├── Database.js # mysql2 pool singleton (Node project)
58
36
  │ └── migrations/ # shared
59
- ├── package.json # Node project only — backend deps + start script
60
- ├── example.config.php # PHP project only — versioned template
61
- ├── example.config.js # Node project only — versioned template
62
- ├── config.php # PHP project only — generated on setup, never commit
63
- └── config.js # Node project only — generated on setup, never commit
37
+ ├── example.config.php / example.config.js # versioned template
38
+ └── config.php / config.js # generated on setup, never commit
64
39
  ```
65
40
 
66
- > `config.php` / `config.js` are generated automatically by `nib new` (a copy of
67
- > the matching `example.config.*`) and are git-ignored, so your secrets stay
68
- > local. The `example.config.*` file is versioned as a secret-free reference. If
69
- > `config.*` is ever missing (e.g. after a fresh clone), copy the example to it.
70
-
71
- > **Where do the Node backend's dependencies live?** In the **root**
72
- > `node_modules`, not in `src/backend` — so your source tree stays clean (no
73
- > `src/backend/node_modules` locally). On the server you install them into
74
- > `out/backend` instead; see `docs/Deploy.md`.
75
-
76
- ## Key difference: how each backend runs
77
-
78
- - **PHP** is executed **per request** by the web server through PHP-FPM (or
79
- FastCGI/mod_php). There is no process to keep alive.
80
- - **Node** is a **long-running process** that *is* the server, listening on
81
- `127.0.0.1:3000`. The web server (Nginx/Apache/IIS) **reverse-proxies** `/api`
82
- to it. You start it once and keep it alive (systemd/pm2). See `docs/Deploy.md`.
83
-
84
- ## Configuration
85
-
86
- `config.php` / `config.js` work like a `.env` file — they hold secrets and
87
- environment settings that stay local and out of version control. Same keys in
88
- both, just PHP-array vs JS-object syntax.
89
-
90
- ### config.php <small>(`src/backend/`)</small>
91
- ```php
92
- // Default key for protected endpoints without a specific key in CUSTOM_ENDPOINT_KEYS
93
- 'GENERAL_API_KEY' => 'DEFAULT_KEY',
94
-
95
- // Per-endpoint keys. For subfolder endpoints use the relative path ('subfolder/endpoint')
96
- 'CUSTOM_ENDPOINT_KEYS' => [
97
- 'subfolder/endpoint' => 'custom-key',
98
- ],
99
-
100
- 'GENERAL_ALLOWED_ORIGINS' => [
101
- '*',
102
- // 'https://example.com',
103
- ],
104
-
105
- 'CUSTOM_ENDPOINT_ORIGINS' => [
106
- 'subfolder/endpoint' => ['https://app.example.com'],
107
- ],
108
-
109
- // Database configuration
110
- 'DB_HOST' => '127.0.0.1',
111
- 'DB_NAME' => 'example_db',
112
- 'DB_USER' => 'root',
113
- 'DB_PASS' => '',
114
- ```
115
-
116
- ### config.js <small>(`src/backend/`)</small>
117
- ```js
118
- module.exports = {
119
- GENERAL_API_KEY: 'DEFAULT_KEY',
120
- CUSTOM_ENDPOINT_KEYS: {
121
- 'subfolder/endpoint': 'custom-key',
122
- },
123
- GENERAL_ALLOWED_ORIGINS: ['*' /*, 'https://example.com' */],
124
- CUSTOM_ENDPOINT_ORIGINS: {
125
- 'subfolder/endpoint': ['https://app.example.com'],
126
- },
127
- DB_HOST: '127.0.0.1',
128
- DB_NAME: 'example_db',
129
- DB_USER: 'root',
130
- DB_PASS: '',
131
- APP_ENV: 'production', // anything other than 'production' = verbose errors
132
- };
133
- ```
134
-
135
- ### API keys
136
-
137
- `GENERAL_API_KEY` is the fallback key for all protected endpoints. Use
138
- `CUSTOM_ENDPOINT_KEYS` to assign a different key to a specific endpoint — for
139
- subfolder endpoints, use the relative path as the key.
140
-
141
- > ⚠️ The API key travels in the `X-Api-Key` header on every request. Use it only
142
- > for server-to-server calls over HTTPS. Never embed it in frontend code, where
143
- > it would be publicly visible.
144
-
145
- ### CORS (allowed origins)
146
-
147
- Cross-origin access mirrors the API-key model:
148
-
149
- - `GENERAL_ALLOWED_ORIGINS` — the default list of origins allowed to call any
150
- endpoint from a browser.
151
- - `CUSTOM_ENDPOINT_ORIGINS` — overrides the default for a specific endpoint,
152
- keyed by the same relative path used in `CUSTOM_ENDPOINT_KEYS`.
153
-
154
- Origins must be exact (scheme + host, no trailing slash), e.g.
155
- `https://example.com`. When a request's `Origin` is in the resolved list, it is
156
- reflected back in `Access-Control-Allow-Origin` (with `Vary: Origin`). An empty
157
- list sends no CORS header — the most restrictive setting; same-origin requests
158
- still work. Use `'*'` as the only element to allow any origin (not recommended
159
- for protected endpoints).
160
-
161
- Resolution order for a given endpoint: `CUSTOM_ENDPOINT_ORIGINS[path]` if
162
- present, otherwise `GENERAL_ALLOWED_ORIGINS`.
41
+ > `config.php` / `config.js` are created automatically by `nib new` as a copy of
42
+ > the matching `example.config.*`, and are git-ignored so your secrets stay
43
+ > local. If `config.*` is missing (e.g. after a fresh clone), copy the example.
163
44
 
164
45
  ## How routing works
165
46
 
166
- The file path inside `api/` maps directly to the URL. The `public`/`protected`
47
+ The file path inside `api/` maps directly to the URL. The `public` / `protected`
167
48
  folder does **not** appear in the URL — it only decides whether the `X-Api-Key`
168
- check applies. So `api/public/example-public` and `api/protected/example-protected`
169
- are both reached at `/api/<endpoint>`:
49
+ check applies:
170
50
 
171
51
  - `api/public/example-public` → `/api/example-public` (no key)
172
52
  - `api/protected/example-protected` → `/api/example-protected` (requires key)
173
53
 
174
- Subfolders **do** appear: `api/public/users/list` → `/api/users/list`. Extra URL
175
- segments become route parameters. Every endpoint has access to:
54
+ Subfolders **do** appear: `api/public/users/list` → `/api/users/list`.
55
+
56
+ Extra URL segments become route parameters, resolved from the deepest match
57
+ backwards: `/api/posts/42` looks for `posts/42` first, then falls back to
58
+ `posts` with `42` as a parameter.
59
+
60
+ Every endpoint receives:
176
61
 
177
- | Variable / field | Description |
62
+ | Field | Description |
178
63
  |---|---|
179
64
  | `method` | HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) |
180
65
  | `requestParams` | Extra URL segments (e.g. `/api/posts/42` → `['42']`) |
181
66
 
182
- > If two files share the same path — one in `public/`, one in `protected/` — the
183
- > public one wins (it is checked first), so the endpoint ends up **without**
184
- > authentication. Don't duplicate names across the two folders.
67
+ The Node handler also gets `query`, `body` (parsed JSON), `rawBody`, `headers`,
68
+ `config`, `req`, `res`.
185
69
 
186
- > **About the examples below** — the code samples use **JavaScript (the Node
187
- > backend)** as the reference. That's just the example language: if you chose the
188
- > **PHP** backend it works exactly the same way, with the same variables, the
189
- > same `Response` helper and the same behaviour — only the syntax differs. Each
190
- > section shows the JS version first, with the equivalent PHP right after.
70
+ > ⚠️ If two files share the same path — one in `public/`, one in `protected/` —
71
+ > the public one wins, so the endpoint ends up **without** authentication. Don't
72
+ > duplicate names across the two folders.
191
73
 
192
- ## Creating a public endpoint
74
+ ## Creating an endpoint
193
75
 
194
- Create a file anywhere inside `api/public/`.
76
+ Create a file anywhere inside `api/public/` or `api/protected/`. For protected
77
+ endpoints the API-key check runs automatically before your file does.
195
78
 
196
- ### `api/public/example.js` (Node — reference)
197
79
  ```js
198
80
  module.exports = ({ method, requestParams, Response }) => {
199
81
  if (method !== 'GET') Response.error('Method not allowed', 405);
@@ -202,95 +84,119 @@ module.exports = ({ method, requestParams, Response }) => {
202
84
  };
203
85
  ```
204
86
 
205
- The Node handler receives a context object: `method`, `requestParams`,
206
- `Response` (`.success` / `.error` / `.noContent`), plus `query`, `body` (parsed
207
- JSON), `rawBody`, `headers`, `config`, `req`, `res`.
208
-
209
- The same endpoint in PHP — identical logic, PHP syntax:
210
- ```php
211
- <?php
212
- declare(strict_types=1);
87
+ ## The Response helper
213
88
 
214
- require_once CORE_PATH . '/modules/Response.php';
89
+ Same envelope in both backends.
215
90
 
216
- if ($method !== 'GET') {
217
- Response::error('Method not allowed', 405);
218
- }
91
+ ```js
92
+ Response.success(data, code); // default 200
93
+ Response.error(message, code, details); // default 400
94
+ Response.noContent(); // 204
95
+ ```
219
96
 
220
- $id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
97
+ Shapes:
221
98
 
222
- Response::success(['id' => $id]);
223
- ```
99
+ - success → `{ "status": "success", "data": ... }`
100
+ - error → `{ "status": "error", "message": "...", "code": ... }` (+ optional `details`)
224
101
 
225
- ## Creating a protected endpoint
102
+ ## Configuration
226
103
 
227
- Create a file inside `api/protected/`. The API-key check happens automatically
228
- before your file runs.
104
+ `config.php` / `config.js` work like a `.env` file: they hold secrets and
105
+ environment settings that stay local and out of version control. Same keys in
106
+ both, PHP-array vs JS-object syntax.
229
107
 
230
- ### `api/protected/example.js` (Node — reference)
231
108
  ```js
232
- module.exports = ({ method, Response }) => {
233
- if (method !== 'GET') Response.error('Method not allowed', 405);
234
- Response.success({ visits: 1024 });
109
+ module.exports = {
110
+ // Default key for protected endpoints that don't have a specific key in CUSTOM_ENDPOINT_KEYS
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')
115
+ CUSTOM_ENDPOINT_KEYS: {
116
+ 'subfolder/example-protected': 'custom-key',
117
+ },
118
+
119
+ GENERAL_ALLOWED_ORIGINS: [
120
+ '*',
121
+ // 'https://example.com',
122
+ ],
123
+
124
+ CUSTOM_ENDPOINT_ORIGINS: {
125
+ 'subfolder/example-protected': ['https://app.example.com'],
126
+ },
127
+
128
+ // Database configuration
129
+ DB_HOST: '127.0.0.1',
130
+ DB_NAME: 'example_db',
131
+ DB_USER: 'root',
132
+ DB_PASS: '',
133
+
134
+ // Environment: 'production' hides error details; anything else = debug.
135
+ APP_ENV: 'production',
235
136
  };
236
137
  ```
237
138
 
238
- The same endpoint in PHP:
239
- ```php
240
- <?php
241
- declare(strict_types=1);
139
+ ### API keys
242
140
 
243
- require_once CORE_PATH . '/modules/Response.php';
141
+ `GENERAL_API_KEY` is the fallback for all protected endpoints.
142
+ `CUSTOM_ENDPOINT_KEYS` assigns a different key to a specific endpoint, keyed by
143
+ its relative path (`'subfolder/endpoint'`).
244
144
 
245
- if ($method !== 'GET') {
246
- Response::error('Method not allowed', 405);
247
- }
145
+ > ⚠️ The key travels in the `X-Api-Key` header on every request. Use it only for
146
+ > server-to-server calls over HTTPS. Never embed it in frontend code, where it
147
+ > would be publicly visible.
248
148
 
249
- Response::success(['visits' => 1024]);
250
- ```
149
+ ### CORS
251
150
 
252
- To assign a dedicated key, add it to your config:
151
+ Mirrors the API-key model: `GENERAL_ALLOWED_ORIGINS` is the default list,
152
+ `CUSTOM_ENDPOINT_ORIGINS` overrides it per endpoint using the same relative path.
253
153
 
254
- ```js
255
- CUSTOM_ENDPOINT_KEYS: { 'endpoint': 'custom-key' } // config.js
256
- ```
257
- ```php
258
- 'CUSTOM_ENDPOINT_KEYS' => [ 'endpoint' => 'custom-key' ], // config.php
259
- ```
154
+ Origins must be exact (scheme + host, no trailing slash). A matching `Origin` is
155
+ reflected in `Access-Control-Allow-Origin` with `Vary: Origin`. An empty list
156
+ sends no CORS header — the most restrictive setting; same-origin requests still
157
+ work. `'*'` as the only element allows any origin (not recommended for protected
158
+ endpoints).
260
159
 
261
- ## The Response helper
160
+ Resolution order: `CUSTOM_ENDPOINT_ORIGINS[path]` if present, otherwise
161
+ `GENERAL_ALLOWED_ORIGINS`.
262
162
 
263
- Identical envelope in both backends — JS shown as reference, PHP works the same:
163
+ ### Error verbosity
264
164
 
265
- ```js
266
- Response.success(data, code); // default 200
267
- Response.error(message, code, details); // default 400
268
- Response.noContent(); // 204
269
- ```
270
- ```php
271
- Response::success($data, $code); // default 200
272
- Response::error($message, $code, $details); // default 400
273
- Response::noContent(); // 204
274
- ```
165
+ `APP_ENV` decides what an unhandled exception tells the client.
275
166
 
276
- Response shapes:
277
- - success → `{ "status": "success", "data": ... }`
278
- - error → `{ "status": "error", "message": "...", "code": ... }` (+ optional `details`)
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.
279
182
 
280
183
  ## Database
281
184
 
282
- Both backends expose a connection singleton reading the same config.
185
+ Both backends expose a connection singleton reading the same config —
186
+ `database/Database.js` (a `mysql2` pool) or `database/Database.php` (a PDO
187
+ singleton).
188
+
189
+ ```js
190
+ const pool = Database.getInstance();
191
+ const [rows] = await pool.execute(sql, params);
192
+ ```
283
193
 
284
- - **Node** (reference) — `database/Database.js`, a `mysql2` pool singleton:
285
- `const pool = Database.getInstance(); const [rows] = await pool.execute(sql, params);`
286
- The `mysql2` driver is installed at scaffold time when you choose Node (it is
287
- loaded lazily, so the backend also boots fine without a database).
288
- - **PHP** — `database/Database.php`, a PDO singleton:
289
- `$pdo = Database::getInstance();`
194
+ `mysql2` is installed at scaffold time and loaded lazily, so the backend boots
195
+ fine without a database.
290
196
 
291
197
  ## Pre-built endpoints
292
198
 
293
199
  | Route | Method | Description |
294
200
  |---|---|---|
295
- | `/api/example-public` | `GET` | Example endpoint that doesn't require any key |
296
- | `/api/example-protected` | `GET` | Example endpoint that requires `X-Api-Key` |
201
+ | `/api/example-public` | `GET` | Example endpoint requiring no key |
202
+ | `/api/example-protected` | `GET` | Example endpoint requiring `X-Api-Key` |
package/docs/Deploy.md CHANGED
@@ -1,135 +1,204 @@
1
1
  # Deploy
2
2
 
3
- Nibula builds a static site into `out`. To publish it you upload that folder to a
4
- web server. What you need depends on **where** you publish and **which backend**
5
- you chose at scaffold time (PHP or Node.js — see `docs/Backend.md`).
3
+ Nibula builds a static site into `out`. Publishing means uploading that folder to
4
+ a web server. What you need depends on **where** you publish and **which
5
+ backend** you chose (see `docs/Backend.md`).
6
6
 
7
- ## Prerequisites
7
+ ## The one constraint that decides everything
8
8
 
9
- **PHP backend:** a server with PHP support (Apache, Nginx + PHP-FPM, or IIS +
10
- FastCGI) and Composer for the backend dependencies. Works on typical shared
11
- hosting.
9
+ The two backends run in fundamentally different ways:
12
10
 
13
- **Node backend:** Node.js 18+ and the ability to run a long-running process —
14
- i.e. a VPS you control, not typical shared hosting. No PHP or Composer required.
11
+ - **PHP** is executed **per request** by the web server through PHP-FPM. There is
12
+ no process to keep alive — the server starts it, it answers, it exits.
13
+ - **Node** **is** the server: a long-running process listening on
14
+ `127.0.0.1:3000`, which the web server reverse-proxies `/api` to. Something has
15
+ to start it and keep it alive across reboots and crashes.
16
+
17
+ This is why **shared hosting cannot run the Node backend**. On Aruba, cPanel and
18
+ similar, you upload files into a web root — you have no shell to start a daemon,
19
+ no systemd, and any process you did manage to launch would be killed. If your
20
+ host is shared hosting, choose PHP. It is not a limitation of Nibula.
21
+
22
+ ## Where you can publish
23
+
24
+ | Target | Frontend | PHP backend | Node backend |
25
+ |---|---|---|---|
26
+ | Shared hosting (Apache — Aruba, cPanel) | ✅ | ✅ | ❌ no persistent process |
27
+ | Windows hosting (IIS) | ✅ | ✅ FastCGI | ⚠️ needs ARR reverse proxy |
28
+ | Your own VPS (Nginx on Ubuntu) | ✅ | ✅ | ✅ |
29
+ | Static hosting (Netlify, Vercel, GitHub Pages) | ✅ | ❌ | ❌ |
30
+
31
+ Static hosting serves files and nothing else — fine if your site has no `/api`
32
+ endpoints, unusable otherwise.
33
+
34
+ ## Requirements
35
+
36
+ **PHP backend:** a server with PHP support and Composer for the backend
37
+ dependencies.
38
+
39
+ **Node backend:** Node.js 18+ and shell access to run a persistent process — a
40
+ VPS you control.
15
41
 
16
42
  ---
17
43
 
18
44
  ## Shared hosting (Apache)
19
45
 
20
- The most common option, and the natural home for the **PHP** backend — shared
21
- hosting can't keep a Node process alive.
46
+ Upload the **contents of `out`** to the web root (often `htdocs`, `public_html`
47
+ or `www`). Done — `.htaccess` is already inside `out` and Apache reads it
48
+ automatically: directory listing off, 403/404 → `/404.html`, sensitive files
49
+ blocked, backend source sealed, `/api/*` routed.
22
50
 
23
- Upload the **contents of `out`** to the hosting's web root (often `htdocs`,
24
- `public_html`, or `www`). Done. `.htaccess` is already inside `out` and Apache
25
- reads it automatically: directory listing off, 403/404 → `/404.html`, sensitive
26
- files blocked, backend source sealed, `/api/*` routed.
27
-
28
- **Backend routing:** if `mod_proxy` is available, `/api` is reverse-proxied to
29
- Node on `127.0.0.1:3000`; if not, `/api` is rewritten to the PHP front
30
- controller. To force PHP on a proxy-enabled server, comment the `mod_proxy`
31
- block in `.htaccess`.
51
+ **Backend routing:** if `mod_proxy` is available, `/api` is proxied to Node on
52
+ `127.0.0.1:3000`; if not — the usual case on shared hosting — `/api` is
53
+ rewritten to the PHP front controller. To force PHP on a proxy-enabled server,
54
+ comment the `mod_proxy` block in `.htaccess`.
32
55
 
33
56
  ## Windows hosting (IIS)
34
57
 
35
- Same idea, different server. Upload the contents of `out`; `web.config` is
36
- already there and read automatically.
58
+ Same idea: upload the contents of `out`, `web.config` is already there and read
59
+ automatically.
60
+
61
+ **Backend routing:** the `ApiToNode` rule (requires the ARR + URL Rewrite
62
+ modules) proxies `/api` to Node and wins if present. Remove it to fall through
63
+ to `ApiToPhp`, which rewrites `/api` to the PHP front controller.
37
64
 
38
- **Backend routing:** the `ApiToNode` rewrite rule (requires the ARR + URL Rewrite
39
- modules) reverse-proxies `/api` to Node and wins if present. Remove it to fall
40
- back to `ApiToPhp`, which rewrites `/api` to the PHP front controller.
65
+ ---
41
66
 
42
67
  ## Your own server (Nginx on a VPS)
43
68
 
44
- The setup for the **Node** backend. Unlike Apache and IIS, Nginx doesn't read
45
- per-directory config files — the whole site is defined in one file.
69
+ Nginx doesn't read per-directory config files — the whole site lives in one file.
70
+ Nibula ships `nginx.conf` in the project root as a ready-to-use site file that
71
+ **covers both backends without edits**: `/api` goes to Node, and if Node is
72
+ unreachable the request falls through to PHP automatically.
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 |
46
85
 
47
- Nibula ships `nginx.conf` in the project root as a **ready-to-use site file**:
48
- replace three placeholders, symlink it, reload.
86
+ ### 2. Install (skip if already present)
49
87
 
50
88
  ```bash
51
- sudo cp nginx.conf /etc/nginx/sites-available/my-site
52
- sudo nano /etc/nginx/sites-available/my-site # YOUR_DOMAIN, SITE_FOLDER, YOUR_CERTIFICATE
53
- sudo ln -s /etc/nginx/sites-available/my-site /etc/nginx/sites-enabled/
54
- sudo nginx -t && sudo systemctl reload nginx
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
55
104
  ```
56
105
 
57
- Full reference, requirements and troubleshooting: **`docs/Nginx.md`**.
106
+ ### 4. Install the site
58
107
 
59
- ## Static hosting (Netlify, Vercel, GitHub Pages)
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
60
115
 
61
- Frontend only. Neither backend runs there — no PHP, no persistent process. Fine
62
- if your site has no `/api` endpoints.
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 |
63
135
 
64
136
  ---
65
137
 
66
138
  ## Starting the Node backend on the server
67
139
 
68
- Unlike PHP, the Node backend is **not** executed by the web server per request —
69
- it is a process you start and keep alive. After uploading your built `out` folder:
140
+ After uploading `out`:
70
141
 
71
- 1. Install the backend's runtime deps (only needed if an endpoint uses the DB):
72
- ```bash
73
- cd /var/www/your-site/out/backend
142
+ 1. Install the backend's runtime deps:
143
+ ```bash
144
+ cd /var/www/SITE_FOLDER/backend
74
145
  npm install
75
- ```
146
+ ```
76
147
 
77
- 2. Make sure `config.js` exists in `out/backend/` (copy `example.config.js` to
78
- `config.js` if missing) and fill in your values.
148
+ 2. Make sure `config.js` exists in `out/backend/` (copy `example.config.js` if
149
+ missing) and fill in your values.
79
150
 
80
- 3. Start it — quick test first:
81
- ```bash
82
- node _core/index.js
83
- # -> [backend-node] listening on http://127.0.0.1:3000 (env: production)
84
- ```
85
- Then, from the server, confirm it answers:
86
- ```bash
151
+ 3. Start it in a new screen:
152
+ ```bash
153
+ screen -S node-backend
154
+ # In the new screen terminal:
155
+ sudo node /var/www/SITE_FOLDER/backend/_core/index.js
87
156
  curl http://127.0.0.1:3000/api/example-public
88
- ```
157
+ ```
89
158
 
90
- > Running `node _core/index.js` directly ties the process to your SSH session —
91
- > close the terminal and the backend dies. To try it out without staying stuck
92
- > there, use **`screen`** (a detachable terminal):
93
- > ```bash
94
- > screen -S backend # open a named session
95
- > node _core/index.js # start the backend inside it
96
- > # press Ctrl+A then D to detach — the backend keeps running
97
- > # reattach later with: screen -r backend
98
- > ```
99
- > `screen` is fine for testing. For production, prefer **systemd** (step 4).
100
-
101
- 4. To keep it running after logout, use **systemd**. An example unit ships at
102
- `backend/backend-node.service.example` — copy it to
103
- `/etc/systemd/system/backend-node.service`, adjust `WorkingDirectory` and the
104
- `node` path (`which node`), then:
105
- ```bash
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
+
184
+ ```bash
106
185
  sudo systemctl daemon-reload
107
186
  sudo systemctl enable --now backend-node
108
187
  sudo systemctl status backend-node
109
- ```
110
- (`pm2` also works: `pm2 start _core/index.js --name backend-node`.)
188
+ ```
111
189
 
112
- The web server then reverse-proxies `/api` to this process. If Node is down the
113
- proxy returns 502 — there is no automatic PHP fallback.
190
+ (`pm2` works too: `pm2 start _core/index.js --name backend-node`.)
114
191
 
115
192
  ### Environment variables
116
193
 
194
+ Read by the Node backend at startup. Set them in the systemd unit with
195
+ `Environment=KEY=value`.
196
+
117
197
  | Variable | Default | Purpose |
118
198
  |---|---|---|
119
- | `PORT` | `3000` | Port Node listens on (match your proxy target) |
120
- | `HOST` | `127.0.0.1` | Bind address (keep it local; the web server is the public face) |
121
- | `APP_ENV` | `production` | `production` hides error details; anything else = verbose 500s |
122
- | `DOCUMENT_ROOT` | auto (`out/`) | Where `404.html` lives; auto-detected as the folder above `backend/` |
123
-
124
- ---
125
-
126
- ## One config file, one backend
127
-
128
- Each server config targets a specific backend, decided at setup time rather than
129
- at runtime:
130
-
131
- - **Apache:** `mod_proxy` present → Node; absent → PHP
132
- - **IIS:** `ApiToNode` rule present (needs ARR) → Node; remove it for `ApiToPhp`
133
- - **Nginx:** ships configured for Node; see `docs/Nginx.md` for PHP
199
+ | `PORT` | `3000` | Port Node listens on (must match the proxy target in `nginx.conf`) |
200
+ | `HOST` | `127.0.0.1` | Bind address — keep it local; the web server is the public face |
201
+ | `DOCUMENT_ROOT` | auto (`SITE_FOLDER/`) | Where `404.html` lives; detected as the folder above `backend/` |
134
202
 
135
- Since a given deployment runs only one backend, this is a one-time setup.
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,42 +1,12 @@
1
1
  # =============================================================================
2
- # Nibula — default nginx site configuration
3
- # Static site + optional HTTP backend on 127.0.0.1:3000. PHP not handled.
2
+ # Nibula — nginx site configuration
4
3
  # -----------------------------------------------------------------------------
5
- # REPLACE
4
+ # Replace the three placeholders below, then follow the setup steps in
5
+ # docs/Deploy.md → "Your own server (Nginx on a VPS)".
6
6
  #
7
- # YOUR_DOMAIN server_name, twice (port 80 and 443)
8
- # SITE_FOLDER document root under /var/www/
9
- #
10
- # YOUR_CERTIFICATE dir under /etc/letsencrypt/live/ — not always the sam
11
- # as YOUR_DOMAIN. Check: sudo certbot certificates
12
- #
13
- # SITE_NAME (commands below) name of this file and of the
14
- # certificate; any short name, never appears in URLs
15
- #
16
- #
17
- # 1. INSTALL (skip if already present)
18
- # sudo apt update
19
- # sudo apt install -y nginx certbot python3-certbot-nginx
20
- # nginx -v # >= 1.25 for the http2 directive
21
- #
22
- # 2. CERTIFICATE (before enabling the site — nginx won't start without it)
23
- # dig +short YOUR_DOMAIN A # must return this server's public IP
24
- # sudo certbot certonly --nginx --cert-name SITE_NAME -d YOUR_DOMAIN
25
- # sudo systemctl enable --now certbot.timer # auto-renew every 90 days
26
- #
27
- # 3. INSTALL THE SITE
28
- # sudo cp nginx.conf /etc/nginx/sites-available/SITE_NAME
29
- # sudo nano /etc/nginx/sites-available/SITE_NAME
30
- # sudo ln -s /etc/nginx/sites-available/SITE_NAME /etc/nginx/sites-enabled/
31
- # sudo nginx -t && sudo systemctl reload nginx
32
- #
33
- # IF IT FAILS
34
- # cannot load certificate ... Permission denied → nginx -t needs sudo
35
- # cannot load certificate ... No such file → wrong certificate name
36
- # no alternative certificate subject name → cert doesn't cover domain
37
- # unknown directive "http2" → nginx < 1.25
38
- # 500 instead of the 404 page → 404.html missing
39
- # NXDOMAIN / timeout during certbot → DNS not pointing here
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/
40
10
  # =============================================================================
41
11
 
42
12
  # HTTP (80) configuration
@@ -63,7 +33,7 @@ server {
63
33
  listen 443 ssl;
64
34
  listen [::]:443 ssl;
65
35
 
66
- # nginx >= 1.25; older: "listen 443 ssl http2;"
36
+ # nginx >= 1.25;
67
37
  http2 on;
68
38
 
69
39
  server_name YOUR_DOMAIN;
@@ -95,7 +65,7 @@ server {
95
65
  return 404;
96
66
  }
97
67
 
98
- location ~* ^/(web\.config)$ {
68
+ location ~* (^|/)web\.config$ {
99
69
  return 404;
100
70
  }
101
71
 
@@ -103,14 +73,11 @@ server {
103
73
  return 404;
104
74
  }
105
75
 
106
- # PHP not handled: without php-fpm these would leak source code.
107
- # For PHP sites, replace this block with fastcgi_pass and change
108
- # try_files below to: $uri $uri/ /index.php?$query_string
109
76
  location ~ \.php$ {
110
77
  return 404;
111
78
  }
112
79
 
113
- ### API backend
80
+ ### API backend — Node primary, PHP fallback
114
81
  location ^~ /api/ {
115
82
  proxy_pass http://127.0.0.1:3000;
116
83
  proxy_http_version 1.1;
@@ -122,9 +89,19 @@ server {
122
89
  # proxy_read_timeout defaults to 60s; raise only for slow endpoints.
123
90
  proxy_connect_timeout 5s;
124
91
 
92
+ proxy_intercept_errors on;
93
+ error_page 502 504 = @php_backend;
94
+
125
95
  # WebSocket is not enabled; needs a $connection_upgrade map at http level.
126
96
  }
127
97
 
98
+ location @php_backend {
99
+ include fastcgi_params;
100
+
101
+ fastcgi_param SCRIPT_FILENAME $document_root/backend/_core/index.php;
102
+ fastcgi_pass unix:/run/php/php-fpm.sock;
103
+ }
104
+
128
105
  ### Static files
129
106
  location / {
130
107
  try_files $uri $uri/ $uri.html =404;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nibula",
3
- "version": "1.4.1",
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
package/docs/Nginx.md DELETED
@@ -1,133 +0,0 @@
1
- # Nginx
2
-
3
- `nginx.conf` in the project root is a **ready-to-use site file** for a Nibula
4
- site on your own server (a VPS). Replace three placeholders, symlink it, reload.
5
-
6
- It targets the **Node** backend; see *Using the PHP backend* below for PHP.
7
-
8
- ## Setup
9
-
10
- | Placeholder | Value |
11
- |---|---|
12
- | `YOUR_DOMAIN` | your domain — appears twice (port 80 and 443) |
13
- | `SITE_FOLDER` | document root under `/var/www/` — your uploaded `out` folder |
14
- | `YOUR_CERTIFICATE` | dir name under `/etc/letsencrypt/live/` — **not always equal to the domain**: a cert may be issued for a different hostname (common with DDNS) or cover several domains. Check with `sudo certbot certificates` |
15
-
16
- ```bash
17
- sudo cp nginx.conf /etc/nginx/sites-available/my-site
18
- sudo nano /etc/nginx/sites-available/my-site # replace the placeholders
19
- sudo ln -s /etc/nginx/sites-available/my-site /etc/nginx/sites-enabled/
20
- sudo nginx -t && sudo systemctl reload nginx
21
- ```
22
-
23
- Verify:
24
-
25
- ```bash
26
- curl -I http://YOUR_DOMAIN/ # 301 to https
27
- curl -I https://YOUR_DOMAIN/ # 200
28
- curl -I https://YOUR_DOMAIN/nonexistent # 404, not 200 or 500
29
- ```
30
-
31
- On RHEL/Alpine there is no `sites-available`; put the file in
32
- `/etc/nginx/conf.d/` instead.
33
-
34
- ## Requirements
35
-
36
- - A valid certificate covering `YOUR_DOMAIN`
37
- - `404.html` present in the document root and readable by nginx
38
- - Nginx ≥ 1.25 for `http2 on;` — on older versions use `listen 443 ssl http2;`
39
-
40
- ## What it provides
41
-
42
- - HTTP → HTTPS redirect on port 80
43
- - TLS via Let's Encrypt, with the Certbot SSL options included
44
- - `server_tokens off` and `autoindex off`
45
- - 403 / 404 → `/404.html`, preserving the original status code
46
- - Backend source directory (`/backend/`) sealed with `return 404`
47
- - `web.config`, dotfiles and `.php` blocked
48
- - Security headers: `X-Content-Type-Options`, `X-Frame-Options`,
49
- `Referrer-Policy`, `Strict-Transport-Security`
50
- - `/api/*` reverse-proxied to the Node backend on `127.0.0.1:3000`
51
-
52
- ## Certificate renewal
53
-
54
- The `/.well-known/acme-challenge/` block is only needed with the **webroot**
55
- authenticator. If you obtained the certificate with `certbot --nginx`, remove it.
56
- Check with:
57
-
58
- ```bash
59
- grep authenticator /etc/letsencrypt/renewal/YOUR_DOMAIN.conf
60
- ```
61
-
62
- When adding a domain to an existing certificate, always pass `--cert-name` **and
63
- list every domain already covered** — omitted ones are dropped, and without the
64
- flag Certbot creates a separate certificate instead of expanding.
65
-
66
- ## HSTS
67
-
68
- The file ships with `max-age=300` deliberately. Browsers cache the HSTS policy
69
- for the whole `max-age` **even after you remove the header**, so a long value
70
- combined with a broken certificate locks visitors out with no way back.
71
-
72
- Raise it to `31536000` once HTTPS is confirmed stable. Add `; includeSubDomains`
73
- only if every current and future subdomain is served over HTTPS with a valid
74
- certificate.
75
-
76
- ## WebSocket
77
-
78
- Not enabled by default. If your backend uses socket.io or similar, add a map at
79
- `http` level (e.g. `/etc/nginx/conf.d/websocket.conf`):
80
-
81
- ```nginx
82
- map $http_upgrade $connection_upgrade {
83
- default upgrade;
84
- '' close;
85
- }
86
- ```
87
-
88
- and inside `location ^~ /api/`:
89
-
90
- ```nginx
91
- proxy_set_header Upgrade $http_upgrade;
92
- proxy_set_header Connection $connection_upgrade;
93
- ```
94
-
95
- For long-lived connections also raise `proxy_read_timeout`, ideally in a
96
- dedicated location for the socket path so normal API requests keep the 60s
97
- default.
98
-
99
- ## Using the PHP backend
100
-
101
- This file targets Node. `.php` requests return 404 by design: without a
102
- `fastcgi_pass` block Nginx would serve them as plain text and leak source code,
103
- database credentials included.
104
-
105
- For a PHP deployment, replace the `.php` block with:
106
-
107
- ```nginx
108
- location ~ \.php$ {
109
- include snippets/fastcgi-php.conf;
110
- fastcgi_pass unix:/run/php/php8.3-fpm.sock;
111
- }
112
- ```
113
-
114
- and change `try_files` in `location /` to `$uri $uri/ /index.php?$query_string`.
115
-
116
- Check the socket path with `ls /run/php/`:
117
-
118
- | Distro | Path |
119
- |---|---|
120
- | Debian / Ubuntu | `unix:/run/php/php8.3-fpm.sock` |
121
- | RHEL / Fedora | `unix:/run/php-fpm/php-fpm.sock` |
122
- | TCP fallback | `127.0.0.1:9000` |
123
-
124
- ## Troubleshooting
125
-
126
- | Symptom | Cause |
127
- |---|---|
128
- | `cannot load certificate ... Permission denied` | `nginx -t` run without `sudo` — the certs are root-only |
129
- | `no alternative certificate subject name matches` | the cert doesn't cover `YOUR_DOMAIN`; check `sudo certbot certificates` |
130
- | 500 instead of the 404 page | `404.html` missing or unreadable by nginx |
131
- | `unknown directive "http2"` | Nginx < 1.25 — use `listen 443 ssl http2;` |
132
- | 502 on `/api/*` | the Node process isn't running; see `docs/Deploy.md` |
133
- | Site not responding at all | symlink missing in `sites-enabled/`, or nginx not reloaded |
@@ -1,30 +0,0 @@
1
- # Example systemd unit to keep the Node backend running on your server.
2
- #
3
- # 1. Copy your built site to the server (the `out` folder).
4
- # 2. Adjust WorkingDirectory below to point at out/backend on your server.
5
- # 3. Copy this file to /etc/systemd/system/backend-node.service (drop the .example).
6
- # 4. Enable and start it:
7
- # sudo systemctl daemon-reload
8
- # sudo systemctl enable --now backend-node
9
- # sudo systemctl status backend-node
10
- #
11
- # Check the Node path with `which node` and update ExecStart if it isn't /usr/bin/node
12
- # (nvm installs live elsewhere, e.g. /home/<user>/.nvm/versions/node/<v>/bin/node).
13
-
14
- [Unit]
15
- Description=Nibula Node backend
16
- After=network.target
17
-
18
- [Service]
19
- Type=simple
20
- WorkingDirectory=/var/www/your-site/out/backend
21
- ExecStart=/usr/bin/node _core/index.js
22
- Environment=PORT=3000
23
- Environment=HOST=127.0.0.1
24
- Environment=APP_ENV=production
25
- Restart=on-failure
26
- RestartSec=2
27
- User=www-data
28
-
29
- [Install]
30
- WantedBy=multi-user.target