nibula 1.5.2 → 1.5.3

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,43 @@ All notable changes to Nibula are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.5.3] - 2026-08-05
9
+
10
+ ### Fixed
11
+ - `web.config` no longer breaks on Windows shared hosting. `ApiToNode` was the
12
+ first rule, so every `/api` request returned a 500: rewriting to an absolute
13
+ URL needs the ARR module, and the `<serverVariables>` block needs an entry in
14
+ `applicationHost.config` — neither is available on a shared plan. `ApiToPhp`
15
+ is now the default and `ApiToNode` ships commented out, with the requirements
16
+ spelled out above it. `<serverVariables>` is gone; on a server with ARR the
17
+ forwarded-protocol header is set server-side.
18
+ - The PHP front controller looked for `404.html` only under
19
+ `$_SERVER['DOCUMENT_ROOT']`, which doesn't resolve to the site root on shared
20
+ hosting, so an unknown `/api` route returned an empty page. It now falls back
21
+ to the path relative to `_core`, matching how `index.js` already resolved it.
22
+ - `Content-Type: text/html` was only sent when `404.html` existed; the plain
23
+ fallback went out undeclared.
24
+ - Requesting a protected endpoint in a browser opened a credentials prompt
25
+ instead of showing the error. IIS attaches `WWW-Authenticate: Basic` to 401
26
+ responses, which the browser answers with a login dialog. It is now removed in
27
+ `web.config`, so the JSON envelope reaches the client as intended.
28
+ - `httpErrors` under `backend/` is set to `PassThrough`. The site root keeps
29
+ `Replace` so unknown pages get `404.html`, but under `backend/` every response
30
+ is the API's own JSON envelope and IIS must not substitute its HTML for it.
31
+ - `nib rename` overwrote the page's SEO title in `site.json` with one derived
32
+ from the new name, discarding whatever you had written. It now moves the
33
+ record under the new key and changes nothing else. This also fixes the rename
34
+ failing outright when the record's `seo` field was malformed, since the field
35
+ is no longer read.
36
+ - `nib rename` rewrote `title` in the route's front matter for the same reason.
37
+ Only `permalink` is updated now.
38
+
39
+ ### Changed
40
+ - `docs/Deploy.md` documents the new IIS default, and warns that a Node project
41
+ published to Windows shared hosting should leave the `backend` folder out —
42
+ the requests would be rewritten onto a PHP front controller the project
43
+ doesn't contain.
44
+
8
45
  ## [1.5.2] - 2026-08-05
9
46
 
10
47
  ### Fixed
package/docs/Deploy.md CHANGED
@@ -58,9 +58,16 @@ comment the `mod_proxy` block in `.htaccess`.
58
58
  Same idea: upload the contents of `out`, `web.config` is already there and read
59
59
  automatically.
60
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.
61
+ **Backend routing:** `/api` is rewritten to the PHP front controller. To use the
62
+ Node backend instead, uncomment the `ApiToNode` rule in `web.config` and delete
63
+ `ApiToPhp` — this needs the ARR and URL Rewrite modules with proxying enabled,
64
+ so a server you control, not shared hosting.
65
+
66
+ > ⚠️ **If you chose the Node backend and your host has no ARR** (Aruba and other
67
+ > Windows shared hosting), don't upload the `backend` folder at all. The static
68
+ > site works, and `/api/*` answers with your `404.html`. Upload it and every
69
+ > `/api` request returns an empty 404 instead: IIS rewrites it onto the PHP
70
+ > front controller, which a Node project doesn't contain.
64
71
 
65
72
  ---
66
73
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nibula",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
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",
@@ -203,7 +203,7 @@ async function handleRequest(req, res) {
203
203
  const apiKey = req.headers['x-api-key'] || '';
204
204
 
205
205
  if (validKey === '' || !hashEquals(validKey, apiKey)) {
206
- Response.error('Unauthorized. X_API_KEY is incorrect or missing', 401);
206
+ Response.error('Unauthorized. X_API_KEY is incorrect or missing', 403);
207
207
  }
208
208
  }
209
209
 
@@ -73,12 +73,24 @@ while (count($checkParts) > 0) {
73
73
  */
74
74
  if (!$endpointFile) {
75
75
  http_response_code(404);
76
+ header_remove('WWW-Authenticate');
77
+
78
+ /**
79
+ * In a Nibula build the backend lives at out/backend/, so the site root
80
+ * (out/) is two levels up from _core. DOCUMENT_ROOT is preferred when it
81
+ * resolves, but on shared hosting it often points elsewhere, so the
82
+ * relative path is used as a fallback. Same resolution as index.js.
83
+ */
84
+ $documentRoot = $_SERVER['DOCUMENT_ROOT'] ?? '';
85
+ $errorPage = $documentRoot . '/404.html';
86
+
87
+ if (!file_exists($errorPage)) {
88
+ $errorPage = __DIR__ . '/../../404.html';
89
+ }
76
90
 
77
- // Look for the 404.html file generated by Eleventy in the server document root
78
- $errorPage = $_SERVER['DOCUMENT_ROOT'] . '/404.html';
91
+ header('Content-Type: text/html; charset=UTF-8');
79
92
 
80
93
  if (file_exists($errorPage)) {
81
- header('Content-Type: text/html; charset=UTF-8');
82
94
  echo file_get_contents($errorPage);
83
95
  } else {
84
96
  echo "<h1>404 Not Found</h1>";
@@ -97,6 +109,7 @@ $endpointPath = preg_replace('#\.php$#', '', str_replace($base, '', $endpointPat
97
109
  // =====================================================
98
110
 
99
111
  header('Content-Type: application/json; charset=UTF-8');
112
+ header_remove('WWW-Authenticate');
100
113
  header('X-Content-Type-Options: nosniff');
101
114
  header('X-Frame-Options: DENY');
102
115
  header('Referrer-Policy: strict-origin-when-cross-origin');
@@ -133,7 +146,7 @@ if ($isProtected) {
133
146
  $apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
134
147
 
135
148
  if ($validKey === '' || !hash_equals($validKey, $apiKey)) {
136
- Response::error('Unauthorized. X_API_KEY is incorrect or missing', 401);
149
+ Response::error('Unauthorized. X_API_KEY is incorrect or missing', 403);
137
150
  }
138
151
  }
139
152
 
@@ -1,17 +1,44 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ IIS config for the Nibula backend folder.
4
+
5
+ The front controller is the only reachable file: every other path is rewritten
6
+ onto it and answered as an API request, so endpoint sources, config.php,
7
+ migrations and vendor files are never served.
8
+
9
+ httpErrors is set to PassThrough here, unlike the site root. Everything under
10
+ this folder is the API, and the front controller already answers errors with
11
+ its own JSON envelope (401 for a missing X-Api-Key, 404 for an unknown
12
+ endpoint, 500 for an unhandled exception). Without PassThrough, IIS replaces
13
+ those bodies with its own HTML and every API error reaches the client in the
14
+ wrong format.
15
+ -->
2
16
  <configuration>
3
17
  <system.webServer>
4
18
  <rewrite>
5
19
  <rules>
20
+
6
21
  <rule name="PassThroughCoreIndex" stopProcessing="true">
7
22
  <match url="^_core/index\.php$" ignoreCase="true" />
8
23
  <action type="None" />
9
24
  </rule>
25
+
10
26
  <rule name="RewriteToFrontController" stopProcessing="true">
11
27
  <match url="^(.*)$" ignoreCase="true" />
12
28
  <action type="Rewrite" url="_core/index.php" appendQueryString="true" />
13
29
  </rule>
30
+
14
31
  </rules>
15
32
  </rewrite>
33
+
34
+ <directoryBrowse enabled="false" />
35
+
36
+ <httpErrors errorMode="Custom" existingResponse="PassThrough" />
37
+
38
+ <httpProtocol>
39
+ <customHeaders>
40
+ <remove name="WWW-Authenticate" />
41
+ </customHeaders>
42
+ </httpProtocol>
16
43
  </system.webServer>
17
44
  </configuration>
@@ -1,18 +1,17 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <!--
3
- IIS config for a Nibula site — covers BOTH backends.
4
-
5
- IIS (like Apache, unlike nginx) cannot health-check an upstream, so it cannot
6
- auto-fall back from Node to PHP. URL Rewrite processes rules top-down and the
7
- first match with stopProcessing="true" wins:
8
-
9
- - "ApiToNode" wins if present -> /api is reverse-proxied to the Node backend
10
- (requires the Application Request Routing + URL Rewrite modules, with proxy
11
- enabled: ARR > Server Proxy Settings > Enable proxy).
12
- - To use the PHP backend instead, remove or comment the "ApiToNode" rule;
13
- "ApiToPhp" then rewrites /api to the PHP front controller.
14
-
15
- On Windows shared hosting without ARR, keep only "ApiToPhp".
3
+ IIS config for a Nibula site.
4
+
5
+ URL Rewrite processes rules top-down and the first match with
6
+ stopProcessing="true" wins. The default is the PHP backend, because it is the
7
+ only one that runs on Windows shared hosting (Aruba and similar), where you
8
+ cannot install server modules.
9
+
10
+ To use the NODE backend instead, uncomment the "ApiToNode" rule below and
11
+ delete "ApiToPhp". This requires a server you control, with the Application
12
+ Request Routing and URL Rewrite modules installed and proxying enabled
13
+ (ARR > Server Proxy Settings > Enable proxy). On shared hosting it will fail
14
+ with a 500: rewriting to an absolute URL without ARR is not allowed.
16
15
  -->
17
16
  <configuration>
18
17
  <system.webServer>
@@ -25,20 +24,18 @@
25
24
  <action type="CustomResponse" statusCode="404" />
26
25
  </rule>
27
26
 
28
- <!-- Node backend (requires ARR). Remove this rule to use PHP. -->
29
- <rule name="ApiToNode" stopProcessing="true">
27
+ <!-- PHP backend: rewrite /api to the PHP front controller. -->
28
+ <rule name="ApiToPhp" stopProcessing="true">
30
29
  <match url="^api/(.*)" ignoreCase="true" />
31
- <action type="Rewrite" url="http://127.0.0.1:3000/api/{R:1}" appendQueryString="true" />
32
- <serverVariables>
33
- <set name="HTTP_X_FORWARDED_PROTO" value="https" />
34
- </serverVariables>
30
+ <action type="Rewrite" url="backend/_core/index.php" appendQueryString="true" />
35
31
  </rule>
36
32
 
37
- <!-- PHP backend fallback: rewrite /api to the PHP front controller. -->
38
- <rule name="ApiToPhp" stopProcessing="true">
33
+ <!-- Node backend (requires ARR). Delete "ApiToPhp" above to use this.
34
+ <rule name="ApiToNode" stopProcessing="true">
39
35
  <match url="^api/(.*)" ignoreCase="true" />
40
- <action type="Rewrite" url="backend/_core/index.php" appendQueryString="true" />
36
+ <action type="Rewrite" url="http://127.0.0.1:3000/api/{R:1}" appendQueryString="true" />
41
37
  </rule>
38
+ -->
42
39
 
43
40
  </rules>
44
41
  </rewrite>
@@ -52,4 +49,4 @@
52
49
  <error statusCode="403" path="/404.html" responseMode="ExecuteURL" />
53
50
  </httpErrors>
54
51
  </system.webServer>
55
- </configuration>
52
+ </configuration>
@@ -18,11 +18,11 @@ function templatePath(name) {
18
18
  return path.join(PATHS.templates, name);
19
19
  }
20
20
 
21
- function applyFrontMatter(filePath, values) {
21
+ function applyFrontMatter(filePath, values, keys = Object.keys(FRONT_MATTER_PATTERNS)) {
22
22
  let content = readText(filePath);
23
23
 
24
- for (const [key, pattern] of Object.entries(FRONT_MATTER_PATTERNS)) {
25
- content = content.replace(pattern, formatText(settings.frontMatter[key], values));
24
+ for (const key of keys) {
25
+ content = content.replace(FRONT_MATTER_PATTERNS[key], formatText(settings.frontMatter[key], values));
26
26
  }
27
27
 
28
28
  writeText(filePath, content);
@@ -96,7 +96,7 @@ function renamePage(oldName, newName) {
96
96
  moveFile(source, destination);
97
97
  log('page.fileRenamed', { source, destination });
98
98
 
99
- if (isRoute) applyFrontMatter(destination, values);
99
+ if (isRoute) applyFrontMatter(destination, values, ['permalink']);
100
100
  }
101
101
  } catch (error) {
102
102
  log('page.renameFailed', { error: error.message });
@@ -79,11 +79,7 @@ function renameSiteData(oldName, newName) {
79
79
  return;
80
80
  }
81
81
 
82
- const record = data[PAGES_KEY][oldCamelName];
83
- data[PAGES_KEY][newCamelName] = {
84
- ...record,
85
- [SEO_KEY]: { ...record[SEO_KEY], [TITLE_KEY]: toTitleCase(newName) },
86
- };
82
+ data[PAGES_KEY][newCamelName] = data[PAGES_KEY][oldCamelName];
87
83
  delete data[PAGES_KEY][oldCamelName];
88
84
 
89
85
  writeSiteData(data);