nibula 1.2.6 → 1.4.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,6 +5,55 @@ 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.0] - 2026-08-03
9
+
10
+ ### Changed
11
+ - `nginx.conf` is now a complete, ready-to-use site file instead of a set of
12
+ directives to paste into your own `server` block. Replace three placeholders
13
+ (`YOUR_DOMAIN`, `SITE_FOLDER`, `YOUR_CERTIFICATE`), symlink it into
14
+ `sites-enabled/`, reload.
15
+ - Nginx documentation moved from `docs/Deploy.md` to `docs/Nginx.md`;
16
+ `Deploy.md` now covers only where to publish, one section per server.
17
+
18
+ ### Added
19
+ - HTTP → HTTPS redirect on port 80, with an optional ACME webroot block.
20
+ - TLS setup via Let's Encrypt, with placeholders for the certificate path.
21
+ - Security headers: `X-Content-Type-Options`, `X-Frame-Options`,
22
+ `Referrer-Policy`, `Strict-Transport-Security` (`max-age=300` by default).
23
+ - `.php` requests return 404, preventing source-code leaks when php-fpm is
24
+ not configured.
25
+
26
+ ### Removed
27
+ - **Breaking:** the automatic Node → PHP fallback on Nginx. `/api` is now
28
+ proxied to Node only; if the process is down, Nginx returns 502. For a PHP
29
+ deployment, edit the `.php` block and `try_files` as described in
30
+ `docs/Nginx.md`.
31
+
32
+ ### Migration
33
+ Existing Nginx deployments: replace your `server` block with the new
34
+ `nginx.conf` and fill in the three placeholders. If you relied on the PHP
35
+ fallback, follow *Using the PHP backend* in `docs/Nginx.md`.
36
+
37
+ ## [1.3.0] - 2026-08-01
38
+
39
+ ### Added
40
+ - A `global.js` module in `src/frontend/js/modules/`, for code that has to run
41
+ on every page — a header menu, a theme toggle, a cookie banner. Page entry
42
+ points import it once and it takes care of running the shared modules, so
43
+ there's no longer a line to remember in every single page.
44
+ - `docs/Javascript.md` now covers the global module, with the header burger
45
+ menu as a worked example and a note on when a behaviour belongs in
46
+ `global.js` rather than in a page.
47
+
48
+ ### Changed
49
+ - Page entry points, and the template new pages are created from, now start
50
+ with `import '../modules/global.js';`.
51
+
52
+ ### Notes
53
+ - Existing projects keep working as they are. To adopt the global module, add
54
+ `src/frontend/js/modules/global.js` and the import line at the top of each
55
+ page entry point.
56
+
8
57
  ## [1.2.6] - 2026-08-01
9
58
 
10
59
  ### Fixed
package/README.md CHANGED
@@ -43,10 +43,10 @@ Nibula ships with a clean, opinionated layout so beginners are never lost. But i
43
43
 
44
44
  * **Node.js**: v18.0.0 or higher
45
45
  * **Composer** *(only if you choose the PHP backend)*: latest version
46
+ * **Better Nunjucks** — VS Code extension by **Ed Heltzel**
46
47
 
47
48
  ### Recommended
48
49
 
49
- * **Better Nunjucks** — VS Code extension by **Ed Heltzel**
50
50
  * **Material Icon Theme** — VS Code extension by **Philipp Kief**
51
51
 
52
52
  ## 📦 Installation
package/docs/Backend.md CHANGED
@@ -11,7 +11,7 @@ Only the language differs.
11
11
  > | Host | PHP | Node.js |
12
12
  > |---|---|---|
13
13
  > | Apache (shared hosting, VPS) | ✅ | ⚠️ needs a persistent process (VPS only) |
14
- > | Nginx + PHP-FPM (VPS) | ✅ | ✅ (Nginx reverse-proxies to Node) |
14
+ > | Nginx (VPS) | ⚠️ works, but the shipped `nginx.conf` targets Node — needs a manual `fastcgi_pass` block | ✅ (Nginx reverse-proxies to Node) |
15
15
  > | IIS (Windows Server) | ✅ FastCGI | ✅ via ARR reverse proxy |
16
16
  > | Static hosting (Netlify, Vercel, GitHub Pages, Cloudflare Pages) | ❌ | ❌ |
17
17
  >
package/docs/Deploy.md CHANGED
@@ -1,13 +1,8 @@
1
- # Server Configuration
1
+ # Deploy
2
2
 
3
- Nibula builds a static site into your output folder (`out`). To put it online you
4
- upload that folder to a web server. Which server config you need depends on
5
- **where** you publish and **which backend** you chose at scaffold time (PHP or
6
- Node.js — see `docs/Backend.md`).
7
-
8
- The shipped `.htaccess`, `web.config` and `nginx.conf` are written to cover
9
- **both** backends, so you don't swap config files based on your choice. There is
10
- one caveat on Apache/IIS, explained at the end.
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`).
11
6
 
12
7
  ## Prerequisites
13
8
 
@@ -15,101 +10,56 @@ one caveat on Apache/IIS, explained at the end.
15
10
  FastCGI) and Composer for the backend dependencies. Works on typical shared
16
11
  hosting.
17
12
 
18
- **Node backend:** Node.js 18+ on the server and the ability to run a
19
- long-running process — i.e. a VPS you control, not typical shared hosting. No PHP
20
- or Composer required.
21
-
22
- ## Where to publish
23
-
24
- ### Shared hosting (Apache — e.g. Aruba, most cPanel providers)
25
-
26
- The most common and beginner-friendly option, and the natural home for the
27
- **PHP** backend (shared hosting can't keep a Node process alive). You rent space
28
- on a server that's already set up and just upload your files.
29
-
30
- Take the **contents of the `out` folder** and upload them to the hosting's public
31
- web root (often `htdocs`, `public_html`, or `www`). That's it — the site is live.
32
- `.htaccess` (Apache) or `web.config` (IIS) are already inside `out` and get picked
33
- up automatically, handling security and routing for you.
34
-
35
- ### Your own server (Nginx — e.g. an Ubuntu VPS)
36
-
37
- If you rent a bare server (a VPS) and install everything yourself, it typically
38
- runs **Nginx** on **Ubuntu Server**. Here nothing is pre-configured: you set up
39
- Nginx, the backend, and the security rules by hand. This is the setup for the
40
- **Node** backend (and also works for PHP + PHP-FPM). The Nginx section below
41
- gives you the essential rules to start from.
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.
42
15
 
43
16
  ---
44
17
 
45
- ## Apache (`.htaccess`)
46
-
47
- `.htaccess` files at `src/frontend/` and `src/backend/` are copied into the build
48
- output by Eleventy. No setup required — upload `out` and Apache reads them.
49
-
50
- They cover:
51
- - Directory listing disabled
52
- - 403 / 404 → `/404.html`
53
- - Sensitive files blocked (`web.config`, dotfiles, etc.)
54
- - Backend source directory sealed — all access funnels through the front controller
55
- - `/api/*` routing (see the dual-backend note below)
18
+ ## Shared hosting (Apache)
56
19
 
57
- **Dual backend:** if `mod_proxy` is available, `/api` is reverse-proxied to the
58
- Node backend (`127.0.0.1:3000`); if not (typical shared hosting), `/api` is
59
- rewritten to the PHP front controller. To force PHP on a server that *has*
60
- `mod_proxy`, comment the `mod_proxy` block in `.htaccess`.
20
+ The most common option, and the natural home for the **PHP** backend — shared
21
+ hosting can't keep a Node process alive.
61
22
 
62
- ## IIS (`web.config`)
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.
63
27
 
64
- For Windows-based hosting. `web.config` files at `src/frontend/` and
65
- `src/backend/` are copied into the build output by Eleventy and read
66
- automatically — same idea as `.htaccess`, different server.
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`.
67
32
 
68
- **Dual backend:** the `ApiToNode` rewrite rule (requires the ARR + URL Rewrite
69
- modules) reverse-proxies `/api` to Node and wins if present. To use PHP instead,
70
- remove the `ApiToNode` rule; the `ApiToPhp` fallback then rewrites `/api` to the
71
- PHP front controller.
33
+ ## Windows hosting (IIS)
72
34
 
73
- ## Nginx
35
+ Same idea, different server. Upload the contents of `out`; `web.config` is
36
+ already there and read automatically.
74
37
 
75
- Used when you run your **own Ubuntu Server** (a VPS). Unlike Apache and IIS, Nginx
76
- does **not** read per-directory config files, so there's nothing automatic here.
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.
77
41
 
78
- **`nginx.conf` in the project root is not a ready-to-use file.** It is a set of
79
- directives you adapt into a complete Nginx `server { }` block — the one that
80
- defines your domain (`server_name`), ports (`listen`), and SSL certificates.
81
- Paste these rules inside your own server block and set `root` to your `out`
82
- folder.
42
+ ## Your own server (Nginx on a VPS)
83
43
 
84
- They provide:
85
- - `server_tokens off` — hides the Nginx version
86
- - `autoindex off` — disables directory listing
87
- - 403 / 404 → `/404.html`
88
- - Backend source directory (`/backend/`) sealed with `return 404`
89
- - Server config files (`.htaccess`, `web.config`) blocked anywhere in the tree
90
- - `/api/*` routed to the backend (see below)
91
- - Security headers on static responses
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.
92
46
 
93
- **Dual backend (auto-fallback):** the shipped `nginx.conf` sends `/api` to the
94
- Node backend on `127.0.0.1:3000` and, if Node is unreachable (502/504), falls
95
- back automatically to the PHP front controller. So the same config works whether
96
- you deployed Node or PHP — Nginx is the only server that can do this fallback
97
- automatically.
47
+ Nibula ships `nginx.conf` in the project root as a **ready-to-use site file**:
48
+ replace three placeholders, symlink it, reload.
98
49
 
99
- > Setting up Nginx fully (SSL, HTTPS redirect, PHP-FPM and/or the Node service)
100
- > goes beyond these rules. These directives are the security starting point, not
101
- > a complete server setup.
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
+ ```
102
56
 
103
- ### PHP-FPM socket
57
+ Full reference, requirements and troubleshooting: **`docs/Nginx.md`**.
104
58
 
105
- When using the PHP path, the socket in the `/api` fallback targets Debian/Ubuntu
106
- by default. Check with `ls /run/php/` and adjust:
59
+ ## Static hosting (Netlify, Vercel, GitHub Pages)
107
60
 
108
- | Distro | Path |
109
- |---|---|
110
- | Debian / Ubuntu | `unix:/run/php/php8.3-fpm.sock` |
111
- | RHEL / Fedora | `unix:/run/php-fpm/php-fpm.sock` |
112
- | TCP fallback | `127.0.0.1:9000` |
61
+ Frontend only. Neither backend runs there — no PHP, no persistent process. Fine
62
+ if your site has no `/api` endpoints.
113
63
 
114
64
  ---
115
65
 
@@ -119,49 +69,48 @@ Unlike PHP, the Node backend is **not** executed by the web server per request
119
69
  it is a process you start and keep alive. After uploading your built `out` folder:
120
70
 
121
71
  1. Install the backend's runtime deps (only needed if an endpoint uses the DB):
122
- ```bash
72
+ ```bash
123
73
  cd /var/www/your-site/out/backend
124
74
  npm install
125
- ```
75
+ ```
126
76
 
127
77
  2. Make sure `config.js` exists in `out/backend/` (copy `example.config.js` to
128
78
  `config.js` if missing) and fill in your values.
129
79
 
130
80
  3. Start it — quick test first:
131
- ```bash
81
+ ```bash
132
82
  node _core/index.js
133
83
  # -> [backend-node] listening on http://127.0.0.1:3000 (env: production)
134
- ```
84
+ ```
135
85
  Then, from the server, confirm it answers:
136
- ```bash
86
+ ```bash
137
87
  curl http://127.0.0.1:3000/api/example-public
138
- ```
88
+ ```
139
89
 
140
90
  > Running `node _core/index.js` directly ties the process to your SSH session —
141
- > close the terminal and the backend dies. The recommended way to try it out
142
- > without staying stuck there is **`screen`** (a detachable terminal):
91
+ > close the terminal and the backend dies. To try it out without staying stuck
92
+ > there, use **`screen`** (a detachable terminal):
143
93
  > ```bash
144
94
  > screen -S backend # open a named session
145
95
  > node _core/index.js # start the backend inside it
146
96
  > # press Ctrl+A then D to detach — the backend keeps running
147
97
  > # reattach later with: screen -r backend
148
98
  > ```
149
- > `screen` is perfect for testing and small setups. For a real production
150
- > deployment that also restarts on crash/reboot, prefer **systemd** (step 4).
99
+ > `screen` is fine for testing. For production, prefer **systemd** (step 4).
151
100
 
152
101
  4. To keep it running after logout, use **systemd**. An example unit ships at
153
102
  `backend/backend-node.service.example` — copy it to
154
103
  `/etc/systemd/system/backend-node.service`, adjust `WorkingDirectory` and the
155
104
  `node` path (`which node`), then:
156
- ```bash
105
+ ```bash
157
106
  sudo systemctl daemon-reload
158
107
  sudo systemctl enable --now backend-node
159
108
  sudo systemctl status backend-node
160
- ```
109
+ ```
161
110
  (`pm2` also works: `pm2 start _core/index.js --name backend-node`.)
162
111
 
163
- The web server then reverse-proxies `/api` to this process; with the shipped
164
- `nginx.conf`, if Node is down Nginx falls back to PHP automatically.
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.
165
114
 
166
115
  ### Environment variables
167
116
 
@@ -174,17 +123,13 @@ The web server then reverse-proxies `/api` to this process; with the shipped
174
123
 
175
124
  ---
176
125
 
177
- ## The one caveat on Apache and IIS
126
+ ## One config file, one backend
178
127
 
179
- Nginx can health-check the Node upstream and fall back to PHP automatically.
180
- Apache (`.htaccess`) and IIS (`web.config`) **cannot** — a per-directory config
181
- can't know whether Node is up. So there the routing is decided by configuration,
182
- not at runtime:
128
+ Each server config targets a specific backend, decided at setup time rather than
129
+ at runtime:
183
130
 
184
- - **Apache:** `mod_proxy` present → Node; absent → PHP. Comment the `mod_proxy`
185
- block to force PHP on a proxy-enabled server.
186
- - **IIS:** `ApiToNode` rule present (needs ARR) → Node; remove it to use the
187
- `ApiToPhp` fallback.
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
188
134
 
189
- Since a given deployment runs only one backend, this is a one-time setup, not a
190
- per-request concern.
135
+ Since a given deployment runs only one backend, this is a one-time setup.
@@ -11,6 +11,7 @@ Import only what the page needs.
11
11
  ### examplePage.js <small>(`src/frontend/js/pages/`)</small>
12
12
 
13
13
  ```js
14
+ import '../modules/global.js';
14
15
  // import { initExampleModule } from '../modules/exampleModule.js';
15
16
 
16
17
  document.addEventListener("DOMContentLoaded", () => {
@@ -20,6 +21,79 @@ document.addEventListener("DOMContentLoaded", () => {
20
21
  // Page logic here
21
22
  ```
22
23
 
24
+ ## Global JS
25
+
26
+ Some code has to run on every page: a header menu, a theme toggle, a cookie banner. Repeating it in every page entry point means you'll eventually forget it on one of them, and the bug only shows up on that single page.
27
+
28
+ That's what `global.js` is for. It lives in `src/frontend/js/modules/`, every page entry point imports it, and it takes care of running the shared modules.
29
+
30
+ ### global.js <small>(`src/frontend/js/modules/`)</small>
31
+
32
+ ```js
33
+ // import { initExampleModule } from './exampleModule.js';
34
+
35
+ function initGlobal() {
36
+ // initExampleModule();
37
+ }
38
+
39
+ // Global logic here
40
+
41
+ // Do not touch this line
42
+ document.addEventListener('DOMContentLoaded', initGlobal);
43
+ ```
44
+
45
+ Pages import it without curly braces, since there is nothing to export — the module runs on its own as soon as it's part of the bundle:
46
+
47
+ ```js
48
+ import '../modules/global.js';
49
+ ```
50
+
51
+ Note that the import path inside `global.js` is `'./exampleModule.js'` and not `'../modules/exampleModule.js'`: `global.js` already sits in the modules folder, next to the modules it imports.
52
+
53
+ ### A concrete example: the header menu
54
+
55
+ A responsive header has a burger button that opens the navigation on small screens. The header is included in `base.njk`, so it's on every page — a textbook case for `global.js`.
56
+
57
+ Write the behaviour as a normal module:
58
+
59
+ ```js
60
+ // src/frontend/js/modules/header.js
61
+
62
+ export function initHeader() {
63
+ const toggle = document.querySelector('.menu-toggle');
64
+ const menu = document.querySelector('.menu');
65
+
66
+ if (!toggle || !menu) return;
67
+
68
+ toggle.addEventListener('click', () => {
69
+ menu.classList.toggle('is-open');
70
+ });
71
+ }
72
+ ```
73
+
74
+ Then wire it up once, in `global.js`:
75
+
76
+ ```js
77
+ import { initHeader } from './header.js';
78
+
79
+ function initGlobal() {
80
+ initHeader();
81
+ }
82
+
83
+ // Do not touch this line
84
+ document.addEventListener('DOMContentLoaded', initGlobal);
85
+ ```
86
+
87
+ The burger now works on every page, and no page entry point had to change. Adding another global behaviour later is two lines: one import, one call inside `initGlobal()`.
88
+
89
+ The early `return` matters: if a page has no header, the module does nothing instead of throwing an error that would stop your other scripts.
90
+
91
+ ### Global or page module?
92
+
93
+ Use `global.js` for behaviour tied to something present on every page — anything included in `base.njk`, essentially.
94
+
95
+ Keep it in the page entry point when only one or two pages need it. Everything imported by `global.js` ends up in *every* page bundle, so a heavy module used by a single page belongs in that page instead.
96
+
23
97
  ## Modules
24
98
 
25
99
  Modules live in `src/frontend/js/modules/`. Modules that interact with the DOM must be called inside `DOMContentLoaded`; others can be called anywhere.
package/docs/Nginx.md ADDED
@@ -0,0 +1,133 @@
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 |
package/nginx.conf CHANGED
@@ -1,75 +1,118 @@
1
- # Example nginx directives for a Nibula site — covers BOTH backends.
1
+ # =============================================================================
2
+ # Nibula — default nginx site configuration
3
+ # Static site + optional HTTP backend on 127.0.0.1:3000. PHP not handled.
4
+ # -----------------------------------------------------------------------------
5
+ # REPLACE
6
+ # YOUR_DOMAIN server_name, twice (port 80 and 443)
7
+ # SITE_FOLDER document root under /var/www/
8
+ # YOUR_CERTIFICATE dir under /etc/letsencrypt/live/ — not always the same
9
+ # as YOUR_DOMAIN. Check: sudo certbot certificates
2
10
  #
3
- # /api is served by Node (index.js) if it is running on 127.0.0.1:3000; if Node
4
- # is unreachable, nginx falls back to the PHP front controller automatically.
5
- # So the same config works whether you deployed the Node backend or the PHP one:
6
- # - Node backend running -> Node answers /api
7
- # - only PHP deployed -> Node is down -> falls back to PHP-FPM
11
+ # REQUIRES
12
+ # valid certificate covering YOUR_DOMAIN
13
+ # /var/www/SITE_FOLDER/404.html readable by nginx
14
+ # nginx >= 1.25 for the http2 directive
8
15
  #
9
- # This is NOT a ready-to-use file: paste these directives inside your own
10
- # server { } block (the one with server_name, listen and SSL) and set `root`
11
- # to your `out` folder.
16
+ # ENABLE
17
+ # sudo ln -s /etc/nginx/sites-available/FILE /etc/nginx/sites-enabled/
18
+ # sudo nginx -t && sudo systemctl reload nginx
19
+ # curl -I https://YOUR_DOMAIN/nonexistent # expect 404
20
+ #
21
+ # IF IT FAILS
22
+ # "cannot load certificate ... Permission denied" → run nginx -t with sudo
23
+ # 500 instead of 404 → 404.html missing
24
+ # =============================================================================
12
25
 
13
- # Hide the nginx version from response headers
14
- server_tokens off;
26
+ ### HTTP -> HTTPS redirect
27
+ server {
28
+ listen 80;
29
+ listen [::]:80;
30
+ server_name YOUR_DOMAIN;
15
31
 
16
- # Force HTTPS on future requests (applies site-wide, including /api)
17
- add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
32
+ # Webroot ACME renewal; drop if using the nginx authenticator.
33
+ location ^~ /.well-known/acme-challenge/ {
34
+ root /var/www/certbot;
35
+ }
18
36
 
19
- root /var/www/your-site/out;
20
- index index.html;
37
+ location / {
38
+ return 301 https://$host$request_uri;
39
+ }
40
+ }
21
41
 
22
- # Disable directory listing
23
- autoindex off;
42
+ server {
43
+ ### Essentials
44
+ listen 443 ssl;
45
+ listen [::]:443 ssl;
24
46
 
25
- error_page 403 404 /404.html;
26
- location = /404.html {
27
- internal;
28
- }
47
+ # nginx >= 1.25; older: "listen 443 ssl http2;"
48
+ http2 on;
29
49
 
30
- # Never expose the backend source directory over HTTP.
31
- location ^~ /backend/ {
32
- return 404;
33
- }
50
+ server_name YOUR_DOMAIN;
51
+ root /var/www/SITE_FOLDER;
52
+ index index.html;
34
53
 
35
- # Block server config files anywhere in the tree (.htaccess, web.config).
36
- location ~* (^|/)(\.htaccess|web\.config)$ {
37
- return 404;
38
- }
54
+ ### Certificate
55
+ ssl_certificate /etc/letsencrypt/live/YOUR_CERTIFICATE/fullchain.pem;
56
+ ssl_certificate_key /etc/letsencrypt/live/YOUR_CERTIFICATE/privkey.pem;
57
+ include /etc/letsencrypt/options-ssl-nginx.conf;
58
+ ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
39
59
 
40
- # --- /api: Node primary, PHP fallback ---------------------------------------
41
- # Try the Node backend first. If it is down (502/504), retry through the PHP
42
- # front controller. Security headers for API responses are set by the backend.
43
- location ^~ /api/ {
44
- proxy_pass http://127.0.0.1:3000;
45
- proxy_http_version 1.1;
46
- proxy_set_header Host $host;
47
- proxy_set_header X-Real-IP $remote_addr;
48
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
49
- proxy_set_header X-Forwarded-Proto $scheme;
50
-
51
- proxy_intercept_errors on;
52
- error_page 502 504 = @php_backend;
53
- }
60
+ ### Security headers
61
+ add_header X-Content-Type-Options "nosniff" always;
62
+ add_header X-Frame-Options "SAMEORIGIN" always;
63
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
54
64
 
55
- location @php_backend {
56
- include fastcgi_params;
57
- fastcgi_param SCRIPT_FILENAME $document_root/backend/_core/index.php;
65
+ # HSTS persists in browsers for max-age even if removed here. Raise to
66
+ # 31536000 once stable; "; includeSubDomains" needs HTTPS on all subdomains.
67
+ add_header Strict-Transport-Security "max-age=300" always;
58
68
 
59
- # Adjust the PHP-FPM socket to your distro:
60
- # Debian / Ubuntu : unix:/run/php/php8.3-fpm.sock
61
- # RHEL / Fedora : unix:/run/php-fpm/php-fpm.sock
62
- # TCP fallback : 127.0.0.1:9000
63
- fastcgi_pass unix:/run/php/php8.3-fpm.sock;
64
- }
69
+ server_tokens off;
70
+ autoindex off;
65
71
 
66
- # Serve static files, fall back to the 404 page.
67
- # A location with add_header does not inherit server-level headers,
68
- # so Strict-Transport-Security is repeated here.
69
- location / {
70
- add_header X-Content-Type-Options "nosniff" always;
71
- add_header X-Frame-Options "SAMEORIGIN" always;
72
- add_header Referrer-Policy "strict-origin-when-cross-origin" always;
73
- add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
74
- try_files $uri $uri/ /404.html;
75
- }
72
+ ### Blocked paths
73
+ error_page 403 404 /404.html;
74
+
75
+ location ^~ /backend/ {
76
+ return 404;
77
+ }
78
+
79
+ location ~* ^/(web\.config)$ {
80
+ return 404;
81
+ }
82
+
83
+ location ~ /\. {
84
+ return 404;
85
+ }
86
+
87
+ # PHP not handled: without php-fpm these would leak source code.
88
+ # For PHP sites, replace this block with fastcgi_pass and change
89
+ # try_files below to: $uri $uri/ /index.php?$query_string
90
+ location ~ \.php$ {
91
+ return 404;
92
+ }
93
+
94
+ ### API backend
95
+ location ^~ /api/ {
96
+ proxy_pass http://127.0.0.1:3000;
97
+ proxy_http_version 1.1;
98
+ proxy_set_header Host $host;
99
+ proxy_set_header X-Real-IP $remote_addr;
100
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
101
+ proxy_set_header X-Forwarded-Proto $scheme;
102
+
103
+ # proxy_read_timeout defaults to 60s; raise only for slow endpoints.
104
+ proxy_connect_timeout 5s;
105
+
106
+ # WebSocket is not enabled; needs a $connection_upgrade map at http level.
107
+ }
108
+
109
+ ### Static files
110
+ location / {
111
+ try_files $uri $uri/ $uri.html =404;
112
+ }
113
+
114
+ ### Error page
115
+ location = /404.html {
116
+ internal;
117
+ }
118
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nibula",
3
- "version": "1.2.6",
3
+ "version": "1.4.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",
@@ -0,0 +1,10 @@
1
+ // import { initExampleModule } from "./exampleModule.js";
2
+
3
+ function initGlobal() {
4
+ // initExampleModule();
5
+ }
6
+
7
+ // Global logic here
8
+
9
+ // Do not touch this line
10
+ document.addEventListener('DOMContentLoaded', initGlobal);
@@ -1,4 +1,5 @@
1
- // import { initExampleModule } from '../modules/exampleModule.js';
1
+ import '../modules/global.js';
2
+ // import { initExampleModule } from '../modules/exampleModule';
2
3
 
3
4
  document.addEventListener("DOMContentLoaded", () => {
4
5
  // initExampleModule();
@@ -1,4 +1,5 @@
1
- // import { initExampleModule } from '../modules/exampleModule.js';
1
+ import '../modules/global.js';
2
+ // import { initExampleModule } from '../modules/exampleModule';
2
3
 
3
4
  document.addEventListener("DOMContentLoaded", () => {
4
5
  // initExampleModule();
@@ -0,0 +1,10 @@
1
+ // import { initExampleModule } from "./exampleModule";
2
+
3
+ function initGlobal() {
4
+ // initExampleModule();
5
+ }
6
+
7
+ // Global logic here
8
+
9
+ // Do not touch this line
10
+ document.addEventListener('DOMContentLoaded', initGlobal);
@@ -1,3 +1,4 @@
1
+ import '../modules/global.js';
1
2
  // import { initExampleModule } from '../modules/exampleModule';
2
3
 
3
4
  document.addEventListener("DOMContentLoaded", (): void => {
@@ -1,3 +1,4 @@
1
+ import '../modules/global.js';
1
2
  // import { initExampleModule } from '../modules/exampleModule';
2
3
 
3
4
  document.addEventListener("DOMContentLoaded", (): void => {
@@ -1,4 +1,5 @@
1
- // import { initExampleModule } from '../modules/exampleModule.js';
1
+ import '../modules/global.js';
2
+ // import { initExampleModule } from '../modules/exampleModule';
2
3
 
3
4
  document.addEventListener("DOMContentLoaded", () => {
4
5
  // initExampleModule();
@@ -1,3 +1,4 @@
1
+ import '../modules/global.js';
1
2
  // import { initExampleModule } from '../modules/exampleModule';
2
3
 
3
4
  document.addEventListener("DOMContentLoaded", (): void => {