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 +1 -4
- package/CHANGELOG.md +68 -11
- package/README.md +11 -23
- package/bin/create.js +5 -5
- package/docs/Backend.md +128 -222
- package/docs/Deploy.md +158 -89
- package/nginx.conf +19 -42
- package/package.json +1 -1
- package/src/backend/example.config.php +13 -3
- package/src/backend/package.json +1 -2
- package/tools/lib/files.js +12 -1
- package/tools/lib/pageActions.js +7 -1
- package/.github/workflows/publish.yml +0 -37
- package/docs/Nginx.md +0 -133
- package/src/backend/backend-node.service.example +0 -30
package/.eleventyignore
CHANGED
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.
|
|
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`
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
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**
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
43
|
-
|
|
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
|
|
52
|
-
│ ├── index.js # Node front controller + server
|
|
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
|
|
57
|
-
│ ├── Database.js # mysql2 pool singleton
|
|
34
|
+
│ ├── Database.php # PDO singleton (PHP project)
|
|
35
|
+
│ ├── Database.js # mysql2 pool singleton (Node project)
|
|
58
36
|
│ └── migrations/ # shared
|
|
59
|
-
├──
|
|
60
|
-
|
|
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
|
|
67
|
-
> the matching `example.config
|
|
68
|
-
> local.
|
|
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
|
|
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
|
|
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`.
|
|
175
|
-
|
|
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
|
-
|
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
>
|
|
187
|
-
>
|
|
188
|
-
>
|
|
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
|
|
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
|
|
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
|
-
|
|
89
|
+
Same envelope in both backends.
|
|
215
90
|
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
97
|
+
Shapes:
|
|
221
98
|
|
|
222
|
-
|
|
223
|
-
|
|
99
|
+
- success → `{ "status": "success", "data": ... }`
|
|
100
|
+
- error → `{ "status": "error", "message": "...", "code": ... }` (+ optional `details`)
|
|
224
101
|
|
|
225
|
-
##
|
|
102
|
+
## Configuration
|
|
226
103
|
|
|
227
|
-
|
|
228
|
-
|
|
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 =
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
239
|
-
```php
|
|
240
|
-
<?php
|
|
241
|
-
declare(strict_types=1);
|
|
139
|
+
### API keys
|
|
242
140
|
|
|
243
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
250
|
-
```
|
|
149
|
+
### CORS
|
|
251
150
|
|
|
252
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
160
|
+
Resolution order: `CUSTOM_ENDPOINT_ORIGINS[path]` if present, otherwise
|
|
161
|
+
`GENERAL_ALLOWED_ORIGINS`.
|
|
262
162
|
|
|
263
|
-
|
|
163
|
+
### Error verbosity
|
|
264
164
|
|
|
265
|
-
|
|
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
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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
|
|
296
|
-
| `/api/example-protected` | `GET` | Example endpoint
|
|
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`.
|
|
4
|
-
web server. What you need depends on **where** you publish and **which
|
|
5
|
-
you chose
|
|
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
|
-
##
|
|
7
|
+
## The one constraint that decides everything
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
**
|
|
14
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
`
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
replace three placeholders, symlink it, reload.
|
|
86
|
+
### 2. Install (skip if already present)
|
|
49
87
|
|
|
50
88
|
```bash
|
|
51
|
-
sudo
|
|
52
|
-
sudo
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
106
|
+
### 4. Install the site
|
|
58
107
|
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
72
|
-
```bash
|
|
73
|
-
cd /var/www/
|
|
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`
|
|
78
|
-
|
|
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
|
|
81
|
-
```bash
|
|
82
|
-
node
|
|
83
|
-
#
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
120
|
-
| `HOST` | `127.0.0.1` | Bind address
|
|
121
|
-
| `
|
|
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
|
-
|
|
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 —
|
|
3
|
-
# Static site + optional HTTP backend on 127.0.0.1:3000. PHP not handled.
|
|
2
|
+
# Nibula — nginx site configuration
|
|
4
3
|
# -----------------------------------------------------------------------------
|
|
5
|
-
#
|
|
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
|
|
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;
|
|
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 ~*
|
|
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.
|
|
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,
|
|
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'
|
|
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'
|
|
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
|
];
|
package/src/backend/package.json
CHANGED
package/tools/lib/files.js
CHANGED
|
@@ -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
|
+
};
|
package/tools/lib/pageActions.js
CHANGED
|
@@ -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
|