nibula 1.5.1 → 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 +45 -0
- package/bin/create.js +1 -3
- package/docs/Deploy.md +10 -3
- package/package.json +1 -1
- package/src/backend/_core/index.js +1 -1
- package/src/backend/_core/index.php +17 -4
- package/src/backend/web.config +27 -0
- package/src/frontend/web.config +20 -23
- package/tools/lib/pageActions.js +4 -4
- package/tools/lib/siteData.js +1 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,51 @@ 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
|
+
|
|
45
|
+
## [1.5.2] - 2026-08-05
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- `nib new` no longer creates an empty `src/frontend/routes` folder. 1.5.1
|
|
49
|
+
removed it from `CREATE_DIRS` but left it in `MANDATORY_COPY`, so it still
|
|
50
|
+
arrived from the template. It is now created on demand when the first page is
|
|
51
|
+
added, and removed again when the last one is deleted.
|
|
52
|
+
|
|
8
53
|
## [1.5.1] - 2026-08-05
|
|
9
54
|
|
|
10
55
|
### Fixed
|
package/bin/create.js
CHANGED
|
@@ -74,9 +74,7 @@ const FRONTEND_EXCLUDE = {
|
|
|
74
74
|
[LANGUAGE.TYPESCRIPT]: ['js'],
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
const CREATE_DIRS = [
|
|
78
|
-
'src/frontend/routes',
|
|
79
|
-
];
|
|
77
|
+
const CREATE_DIRS = [];
|
|
80
78
|
|
|
81
79
|
// Backend files that belong to exactly one backend, matched by basename.
|
|
82
80
|
// Everything else (migrations, .htaccess, web.config, README, ...) is shared.
|
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:**
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
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',
|
|
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
|
-
|
|
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',
|
|
149
|
+
Response::error('Unauthorized. X_API_KEY is incorrect or missing', 403);
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
|
package/src/backend/web.config
CHANGED
|
@@ -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>
|
package/src/frontend/web.config
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
2
|
<!--
|
|
3
|
-
IIS config for a Nibula site
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
<!--
|
|
29
|
-
<rule name="
|
|
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="
|
|
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
|
-
<!--
|
|
38
|
-
<rule name="
|
|
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="
|
|
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>
|
package/tools/lib/pageActions.js
CHANGED
|
@@ -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
|
|
25
|
-
content = content.replace(
|
|
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 });
|
package/tools/lib/siteData.js
CHANGED
|
@@ -79,11 +79,7 @@ function renameSiteData(oldName, newName) {
|
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
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);
|