nibula 1.4.1 → 1.5.0

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/CHANGELOG.md CHANGED
@@ -5,21 +5,35 @@ 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.0] - 2026-08-04
9
9
 
10
10
  ### 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.
11
+ - `nginx.conf` now covers **both backends** with no manual edits. `/api` is
12
+ proxied to Node on `127.0.0.1:3000`; if Node is unreachable, the request is
13
+ retried through the PHP front controller via the internal `@php_backend`
14
+ location. The same file works for a Node deployment and a PHP one.
15
+ - `nginx.conf` header now documents the PHP-FPM requirement, the version-agnostic
16
+ socket, and the errors specific to a PHP deployment.
18
17
 
19
18
  ### 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.
19
+ - PHP is invoked through `SCRIPT_FILENAME` instead of by URL, so `.php` requests
20
+ keep returning 404 even on a PHP deployment and no source file is ever
21
+ reachable by name.
22
+ - `fastcgi_pass` targets the version-agnostic socket `unix:/run/php/php-fpm.sock`
23
+ instead of a versioned path, so the config survives PHP upgrades.
24
+ - `web.config` is now blocked anywhere in the tree, not only at the document root.
25
+ - `docs/Backend.md` is limited to writing endpoints: routing, `public` vs
26
+ `protected`, the `Response` helper and configuration. Examples are Node-only,
27
+ with a note that PHP behaves identically. Hosting and server requirements
28
+ moved to `docs/Deploy.md`.
29
+ - `docs/Deploy.md` rewritten around where a site can be published and what each
30
+ target requires, including why shared hosting cannot run the Node backend.
31
+
32
+ ### Removed
33
+ - `docs/Nginx.md` — the setup steps live in the `nginx.conf` header, where they
34
+ are read at the moment they are needed.
35
+ - `src/backend/backend-node.service.example` — the systemd unit is documented
36
+ in `docs/Deploy.md` instead of shipped as a file.
23
37
 
24
38
  ## [1.4.0] - 2026-08-03
25
39
 
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
64
- ```
65
-
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
- };
37
+ ├── example.config.php / example.config.js # versioned template
38
+ └── config.php / config.js # generated on setup, never commit
133
39
  ```
134
40
 
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`.
176
55
 
177
- | Variable / field | Description |
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:
61
+
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,86 @@ 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
+ GENERAL_API_KEY: 'DEFAULT_KEY',
111
+ CUSTOM_ENDPOINT_KEYS: {
112
+ 'subfolder/endpoint': 'custom-key',
113
+ },
114
+ GENERAL_ALLOWED_ORIGINS: ['*' /*, 'https://example.com' */],
115
+ CUSTOM_ENDPOINT_ORIGINS: {
116
+ 'subfolder/endpoint': ['https://app.example.com'],
117
+ },
118
+ DB_HOST: '127.0.0.1',
119
+ DB_NAME: 'example_db',
120
+ DB_USER: 'root',
121
+ DB_PASS: '',
122
+ APP_ENV: 'production', // anything else = verbose errors
235
123
  };
236
124
  ```
237
125
 
238
- The same endpoint in PHP:
239
- ```php
240
- <?php
241
- declare(strict_types=1);
126
+ ### API keys
242
127
 
243
- require_once CORE_PATH . '/modules/Response.php';
128
+ `GENERAL_API_KEY` is the fallback for all protected endpoints.
129
+ `CUSTOM_ENDPOINT_KEYS` assigns a different key to a specific endpoint, keyed by
130
+ its relative path (`'subfolder/endpoint'`).
244
131
 
245
- if ($method !== 'GET') {
246
- Response::error('Method not allowed', 405);
247
- }
132
+ > ⚠️ The key travels in the `X-Api-Key` header on every request. Use it only for
133
+ > server-to-server calls over HTTPS. Never embed it in frontend code, where it
134
+ > would be publicly visible.
248
135
 
249
- Response::success(['visits' => 1024]);
250
- ```
136
+ ### CORS
251
137
 
252
- To assign a dedicated key, add it to your config:
138
+ Mirrors the API-key model: `GENERAL_ALLOWED_ORIGINS` is the default list,
139
+ `CUSTOM_ENDPOINT_ORIGINS` overrides it per endpoint using the same relative path.
253
140
 
254
- ```js
255
- CUSTOM_ENDPOINT_KEYS: { 'endpoint': 'custom-key' } // config.js
256
- ```
257
- ```php
258
- 'CUSTOM_ENDPOINT_KEYS' => [ 'endpoint' => 'custom-key' ], // config.php
259
- ```
141
+ Origins must be exact (scheme + host, no trailing slash). A matching `Origin` is
142
+ reflected in `Access-Control-Allow-Origin` with `Vary: Origin`. An empty list
143
+ sends no CORS header — the most restrictive setting; same-origin requests still
144
+ work. `'*'` as the only element allows any origin (not recommended for protected
145
+ endpoints).
260
146
 
261
- ## The Response helper
147
+ Resolution order: `CUSTOM_ENDPOINT_ORIGINS[path]` if present, otherwise
148
+ `GENERAL_ALLOWED_ORIGINS`.
149
+
150
+ ## Database
262
151
 
263
- Identical envelope in both backends — JS shown as reference, PHP works the same:
152
+ Both backends expose a connection singleton reading the same config —
153
+ `database/Database.js` (a `mysql2` pool) or `database/Database.php` (a PDO
154
+ singleton).
264
155
 
265
156
  ```js
266
- Response.success(data, code); // default 200
267
- Response.error(message, code, details); // default 400
268
- Response.noContent(); // 204
157
+ const pool = Database.getInstance();
158
+ const [rows] = await pool.execute(sql, params);
269
159
  ```
270
- ```php
271
- Response::success($data, $code); // default 200
272
- Response::error($message, $code, $details); // default 400
273
- Response::noContent(); // 204
274
- ```
275
-
276
- Response shapes:
277
- - success → `{ "status": "success", "data": ... }`
278
- - error → `{ "status": "error", "message": "...", "code": ... }` (+ optional `details`)
279
-
280
- ## Database
281
-
282
- Both backends expose a connection singleton reading the same config.
283
160
 
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();`
161
+ `mysql2` is installed at scaffold time and loaded lazily, so the backend boots
162
+ fine without a database.
290
163
 
291
164
  ## Pre-built endpoints
292
165
 
293
166
  | Route | Method | Description |
294
167
  |---|---|---|
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` |
168
+ | `/api/example-public` | `GET` | Example endpoint requiring no key |
169
+ | `/api/example-protected` | `GET` | Example endpoint requiring `X-Api-Key` |
package/docs/Deploy.md CHANGED
@@ -1,135 +1,114 @@
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.
15
16
 
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.
17
21
 
18
- ## Shared hosting (Apache)
22
+ ## Where you can publish
19
23
 
20
- The most common option, and the natural home for the **PHP** backend — shared
21
- hosting can't keep a Node process alive.
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) | ✅ | ❌ | ❌ |
22
30
 
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.
31
+ Static hosting serves files and nothing else — fine if your site has no `/api`
32
+ endpoints, unusable otherwise.
27
33
 
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`.
34
+ ## Requirements
32
35
 
33
- ## Windows hosting (IIS)
36
+ **PHP backend:** a server with PHP support and Composer for the backend
37
+ dependencies.
34
38
 
35
- Same idea, different server. Upload the contents of `out`; `web.config` is
36
- already there and read automatically.
39
+ **Node backend:** Node.js 18+ and shell access to run a persistent process — a
40
+ VPS you control.
37
41
 
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.
42
+ ---
41
43
 
42
- ## Your own server (Nginx on a VPS)
44
+ ## Shared hosting (Apache)
43
45
 
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.
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.
46
50
 
47
- Nibula ships `nginx.conf` in the project root as a **ready-to-use site file**:
48
- replace three placeholders, symlink it, reload.
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`.
49
55
 
50
- ```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
55
- ```
56
+ ## Windows hosting (IIS)
56
57
 
57
- Full reference, requirements and troubleshooting: **`docs/Nginx.md`**.
58
+ Same idea: upload the contents of `out`, `web.config` is already there and read
59
+ automatically.
58
60
 
59
- ## Static hosting (Netlify, Vercel, GitHub Pages)
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.
60
64
 
61
- Frontend only. Neither backend runs there — no PHP, no persistent process. Fine
62
- if your site has no `/api` endpoints.
65
+ ## Your own server (Nginx on a VPS)
66
+
67
+ Nginx doesn't read per-directory config files — the whole site lives in one file.
68
+ Nibula ships `nginx.conf` in the project root as a ready-to-use site file that
69
+ **covers both backends without edits**: `/api` goes to Node, and if Node is
70
+ unreachable the request falls through to PHP automatically.
63
71
 
64
72
  ---
65
73
 
66
74
  ## Starting the Node backend on the server
67
75
 
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:
76
+ After uploading `out`:
70
77
 
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
78
+ 1. Install the backend's runtime deps:
79
+ ```bash
80
+ cd /var/www/backend/SITE_FOLDER
74
81
  npm install
75
- ```
76
-
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.
79
-
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
82
+ ```
83
+
84
+ 2. Make sure `config.js` exists in `out/backend/` (copy `example.config.js` if
85
+ missing) and fill in your values.
86
+
87
+ 3. Start it in a new screen:
88
+ ```bash
89
+ screen -S node-backend
90
+ # In the new screen terminal:
91
+ sudo node /var/www/SITE_FOLDER/backend/_core/index.js
87
92
  curl http://127.0.0.1:3000/api/example-public
88
- ```
89
-
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
93
+ ```
94
+ 4. Exit the screen without closing the process with `CTRL + A`, `D`. To resume the screen type `screen -r node-backend`.
95
+
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
103
98
  `/etc/systemd/system/backend-node.service`, adjust `WorkingDirectory` and the
104
99
  `node` path (`which node`), then:
105
- ```bash
100
+ ```bash
106
101
  sudo systemctl daemon-reload
107
102
  sudo systemctl enable --now backend-node
108
103
  sudo systemctl status backend-node
109
- ```
110
- (`pm2` also works: `pm2 start _core/index.js --name backend-node`.)
111
-
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.
104
+ ```
105
+ (`pm2` works too: `pm2 start _core/index.js --name backend-node`.)
114
106
 
115
107
  ### Environment variables
116
108
 
117
109
  | Variable | Default | Purpose |
118
110
  |---|---|---|
119
111
  | `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) |
112
+ | `HOST` | `127.0.0.1` | Bind address — keep it local; the web server is the public face |
121
113
  | `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
134
-
135
- Since a given deployment runs only one backend, this is a one-time setup.
114
+ | `DOCUMENT_ROOT` | auto (`SITE_FOLDER/`) | Where `404.html` lives; detected as the folder above `backend/` |
package/nginx.conf CHANGED
@@ -1,42 +1,49 @@
1
1
  # =============================================================================
2
2
  # Nibula — default nginx site configuration
3
- # Static site + optional HTTP backend on 127.0.0.1:3000. PHP not handled.
4
3
  # -----------------------------------------------------------------------------
5
- # REPLACE
4
+ # 1. REPLACE
6
5
  #
7
6
  # YOUR_DOMAIN server_name, twice (port 80 and 443)
8
7
  # SITE_FOLDER document root under /var/www/
8
+ # YOUR_CERTIFICATE dir under /etc/letsencrypt/live/
9
+ # SITE_NAME commands below file name
9
10
  #
10
- # YOUR_CERTIFICATE dir under /etc/letsencrypt/live/ — not always the sam
11
- # as YOUR_DOMAIN. Check: sudo certbot certificates
11
+ # 2. INSTALL (skip if already present)
12
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
13
  # sudo apt update
19
14
  # sudo apt install -y nginx certbot python3-certbot-nginx
20
- # nginx -v # >= 1.25 for the http2 directive
21
15
  #
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
16
+ # If you are using PHP as backend:
17
+ # sudo apt install -y php-fpm
18
+ #
19
+ # 3. CERTIFICATE
20
+ #
24
21
  # sudo certbot certonly --nginx --cert-name SITE_NAME -d YOUR_DOMAIN
25
22
  # sudo systemctl enable --now certbot.timer # auto-renew every 90 days
26
23
  #
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
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
32
  #
33
- # IF IT FAILS
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
34
40
  # cannot load certificate ... Permission denied → nginx -t needs sudo
35
41
  # 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
42
+ # no alternative certificate subject name → certificate doesn't cover domain
43
+ # unknown directive "http2" → nginx < 1.25, check with nginx -v
38
44
  # 500 instead of the 404 page → 404.html missing
39
45
  # NXDOMAIN / timeout during certbot → DNS not pointing here
46
+ # 502 on /api with PHP deployed → wrong php-fpm socket path
40
47
  # =============================================================================
41
48
 
42
49
  # HTTP (80) configuration
@@ -63,7 +70,7 @@ server {
63
70
  listen 443 ssl;
64
71
  listen [::]:443 ssl;
65
72
 
66
- # nginx >= 1.25; older: "listen 443 ssl http2;"
73
+ # nginx >= 1.25;
67
74
  http2 on;
68
75
 
69
76
  server_name YOUR_DOMAIN;
@@ -95,7 +102,7 @@ server {
95
102
  return 404;
96
103
  }
97
104
 
98
- location ~* ^/(web\.config)$ {
105
+ location ~* (^|/)web\.config$ {
99
106
  return 404;
100
107
  }
101
108
 
@@ -103,14 +110,11 @@ server {
103
110
  return 404;
104
111
  }
105
112
 
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
113
  location ~ \.php$ {
110
114
  return 404;
111
115
  }
112
116
 
113
- ### API backend
117
+ ### API backend — Node primary, PHP fallback
114
118
  location ^~ /api/ {
115
119
  proxy_pass http://127.0.0.1:3000;
116
120
  proxy_http_version 1.1;
@@ -122,9 +126,19 @@ server {
122
126
  # proxy_read_timeout defaults to 60s; raise only for slow endpoints.
123
127
  proxy_connect_timeout 5s;
124
128
 
129
+ proxy_intercept_errors on;
130
+ error_page 502 504 = @php_backend;
131
+
125
132
  # WebSocket is not enabled; needs a $connection_upgrade map at http level.
126
133
  }
127
134
 
135
+ location @php_backend {
136
+ include fastcgi_params;
137
+
138
+ fastcgi_param SCRIPT_FILENAME $document_root/backend/_core/index.php;
139
+ fastcgi_pass unix:/run/php/php-fpm.sock;
140
+ }
141
+
128
142
  ### Static files
129
143
  location / {
130
144
  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.0",
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",
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