nibula 1.0.1 → 1.1.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/README.md +20 -6
- package/_tools/modules/pageComponents.js +1 -1
- package/bin/create.js +39 -3
- package/docs/Backend.md +179 -42
- package/docs/Deploy.md +139 -50
- package/nginx.conf +75 -47
- package/package.json +1 -1
- package/src/backend/_core/index.js +267 -0
- package/src/backend/_core/init.js +52 -0
- package/src/backend/_core/modules/RateLimiter.js +58 -0
- package/src/backend/_core/modules/Response.js +59 -0
- package/src/backend/api/protected/example-protected.js +23 -0
- package/src/backend/api/public/example-public.js +24 -0
- package/src/backend/backend-node.service.example +30 -0
- package/src/backend/database/Database.js +46 -0
- package/src/backend/example.config.js +37 -0
- package/src/backend/package.json +18 -0
- package/src/frontend/.htaccess +51 -16
- package/src/frontend/data/site.json +54 -54
- package/src/frontend/web.config +55 -27
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mirror of src/backend/_core/init.php
|
|
5
|
+
*
|
|
6
|
+
* init.php did three things:
|
|
7
|
+
* 1. registered a global exception handler that turns any throwable into a
|
|
8
|
+
* 500 JSON response (verbose in debug, generic in production);
|
|
9
|
+
* 2. registered an error handler that promoted PHP warnings to exceptions;
|
|
10
|
+
* 3. loaded config.php.
|
|
11
|
+
*
|
|
12
|
+
* In Node there is no per-request global handler, so the exception-to-500
|
|
13
|
+
* translation lives in the front controller (index.js), which calls
|
|
14
|
+
* handleException() below. Here we expose config loading and that helper.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load configuration. dirname(__dirname) in PHP pointed at the folder holding
|
|
22
|
+
* config.php (the backend root). Here that is the parent of _core.
|
|
23
|
+
* If config.js is missing (e.g. fresh clone) we fall back to example.config.js,
|
|
24
|
+
* mirroring the "copy example.config to config" guidance.
|
|
25
|
+
*/
|
|
26
|
+
function loadConfig() {
|
|
27
|
+
const backendRoot = path.join(__dirname, '..');
|
|
28
|
+
const configPath = path.join(backendRoot, 'config.js');
|
|
29
|
+
const examplePath = path.join(backendRoot, 'example.config.js');
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(configPath)) {
|
|
32
|
+
return require(configPath);
|
|
33
|
+
}
|
|
34
|
+
return require(examplePath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Turn any thrown error into a 500 response, mirroring set_exception_handler.
|
|
39
|
+
* @param {Error} exception
|
|
40
|
+
* @param {object} config
|
|
41
|
+
* @param {object} Response request-bound helper
|
|
42
|
+
*/
|
|
43
|
+
function handleException(exception, config, Response) {
|
|
44
|
+
const isDebug = (config.APP_ENV || 'production') !== 'production';
|
|
45
|
+
Response.error(
|
|
46
|
+
isDebug ? exception.message : 'Internal server error',
|
|
47
|
+
500,
|
|
48
|
+
isDebug ? { stack: exception.stack } : null
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { loadConfig, handleException };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mirror of src/backend/_core/modules/RateLimiter.php
|
|
5
|
+
*
|
|
6
|
+
* Same algorithm and same on-disk format: one JSON file per IP under ../../cache,
|
|
7
|
+
* holding an array of request timestamps. Old timestamps outside the window are
|
|
8
|
+
* pruned, the current one appended, and if the count exceeds the limit a 429 is
|
|
9
|
+
* returned (with a Retry-After header) through the request-bound Response helper.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
class RateLimiter {
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} ip
|
|
19
|
+
* @param {number} maxRequests
|
|
20
|
+
* @param {number} windowSeconds
|
|
21
|
+
* @param {object} Response request-bound helper from createResponse(res)
|
|
22
|
+
* @param {object} res raw http response (to set Retry-After)
|
|
23
|
+
*/
|
|
24
|
+
static check(ip, maxRequests = 60, windowSeconds = 60, Response, res) {
|
|
25
|
+
const cacheDir = path.join(__dirname, '..', '..', 'cache');
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(cacheDir)) {
|
|
28
|
+
fs.mkdirSync(cacheDir, { recursive: true, mode: 0o755 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const hash = crypto.createHash('md5').update(ip).digest('hex');
|
|
32
|
+
const cacheFile = path.join(cacheDir, 'rl_' + hash + '.json');
|
|
33
|
+
const now = Math.floor(Date.now() / 1000);
|
|
34
|
+
let data = [];
|
|
35
|
+
|
|
36
|
+
if (fs.existsSync(cacheFile)) {
|
|
37
|
+
try {
|
|
38
|
+
data = JSON.parse(fs.readFileSync(cacheFile, 'utf8')) || [];
|
|
39
|
+
} catch (e) {
|
|
40
|
+
data = [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
data = data.filter((ts) => ts > (now - windowSeconds));
|
|
45
|
+
data.push(now);
|
|
46
|
+
|
|
47
|
+
// LOCK_EX equivalent: an exclusive write. Node's writeFileSync is atomic
|
|
48
|
+
// enough for this single-process model.
|
|
49
|
+
fs.writeFileSync(cacheFile, JSON.stringify(data));
|
|
50
|
+
|
|
51
|
+
if (data.length > maxRequests) {
|
|
52
|
+
res.setHeader('Retry-After', String(windowSeconds));
|
|
53
|
+
Response.error('Too Many Requests', 429);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = RateLimiter;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mirror of src/backend/_core/modules/Response.php
|
|
5
|
+
*
|
|
6
|
+
* In PHP the Response helper writes JSON and calls exit() to stop execution.
|
|
7
|
+
* Node has no per-request exit, so each method writes to the response and then
|
|
8
|
+
* throws the HALT sentinel. The front controller catches HALT and stops the
|
|
9
|
+
* dispatch, reproducing the same "the endpoint stops here" behaviour.
|
|
10
|
+
*
|
|
11
|
+
* A fresh, response-bound helper is created per request via createResponse(res)
|
|
12
|
+
* so overlapping async requests never clash (PHP was process-per-request).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Sentinel thrown to unwind execution after a response has been sent (≈ PHP exit).
|
|
16
|
+
const HALT = Symbol('RESPONSE_HALT');
|
|
17
|
+
|
|
18
|
+
function jsonEncode(obj) {
|
|
19
|
+
// JSON.stringify already leaves unicode unescaped and does not escape slashes,
|
|
20
|
+
// matching PHP's JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES.
|
|
21
|
+
return JSON.stringify(obj);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createResponse(res) {
|
|
25
|
+
return {
|
|
26
|
+
success(data = null, code = 200) {
|
|
27
|
+
res.statusCode = code;
|
|
28
|
+
res.setHeader('Content-Type', 'application/json; charset=UTF-8');
|
|
29
|
+
res.end(jsonEncode({
|
|
30
|
+
status: 'success',
|
|
31
|
+
data: data,
|
|
32
|
+
}));
|
|
33
|
+
throw HALT;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
error(message, code = 400, details = null) {
|
|
37
|
+
res.statusCode = code;
|
|
38
|
+
res.setHeader('Content-Type', 'application/json; charset=UTF-8');
|
|
39
|
+
const body = {
|
|
40
|
+
status: 'error',
|
|
41
|
+
message: message,
|
|
42
|
+
code: code,
|
|
43
|
+
};
|
|
44
|
+
if (details !== null) {
|
|
45
|
+
body.details = details;
|
|
46
|
+
}
|
|
47
|
+
res.end(jsonEncode(body));
|
|
48
|
+
throw HALT;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
noContent() {
|
|
52
|
+
res.statusCode = 204;
|
|
53
|
+
res.end();
|
|
54
|
+
throw HALT;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { createResponse, HALT };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mirror of src/backend/api/protected/example-protected.php
|
|
5
|
+
*
|
|
6
|
+
* Protected endpoint: the API key check (X-Api-Key header) runs automatically in
|
|
7
|
+
* the front controller before this file is loaded, so here we just handle logic.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
module.exports = ({ method, requestParams, Response }) => {
|
|
11
|
+
if (method !== 'GET') {
|
|
12
|
+
Response.error('Method not allowed', 405);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//
|
|
16
|
+
// Your endpoint logic here. You can access route parameters in requestParams
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
Response.success({
|
|
20
|
+
message: 'Protected endpoint is working',
|
|
21
|
+
params: requestParams,
|
|
22
|
+
});
|
|
23
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mirror of src/backend/api/public/example-public.php
|
|
5
|
+
*
|
|
6
|
+
* Public endpoint: no API key required. Receives the request context and calls
|
|
7
|
+
* the request-bound Response helper, exactly like the PHP file used the global
|
|
8
|
+
* Response class and $method / $requestParams.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
module.exports = ({ method, requestParams, Response }) => {
|
|
12
|
+
if (method !== 'GET') {
|
|
13
|
+
Response.error('Method not allowed', 405);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
//
|
|
17
|
+
// Your endpoint logic here. You can access route parameters in requestParams
|
|
18
|
+
//
|
|
19
|
+
|
|
20
|
+
Response.success({
|
|
21
|
+
message: 'Public endpoint is working',
|
|
22
|
+
params: requestParams,
|
|
23
|
+
});
|
|
24
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mirror of src/backend/database/Database.php
|
|
5
|
+
*
|
|
6
|
+
* PDO singleton → a single shared mysql2 connection pool. Config is read from
|
|
7
|
+
* ../config.js (same relative location as the PHP version's ../config.php).
|
|
8
|
+
* Options map the PDO attributes as closely as mysql2 allows:
|
|
9
|
+
* - ERRMODE_EXCEPTION → mysql2 rejects/throws on error by default
|
|
10
|
+
* - FETCH_ASSOC → rowsAsArray:false (objects keyed by column) is default
|
|
11
|
+
* - EMULATE_PREPARES false → native prepared statements via pool.execute()
|
|
12
|
+
*
|
|
13
|
+
* Requires the "mysql2" package (npm install mysql2). getInstance() returns the
|
|
14
|
+
* promise-based pool; use pool.execute(sql, params) for prepared statements.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
let instance = null;
|
|
20
|
+
|
|
21
|
+
class Database {
|
|
22
|
+
static getInstance() {
|
|
23
|
+
if (instance === null) {
|
|
24
|
+
// Lazy require so the backend boots even without a DB configured.
|
|
25
|
+
// eslint-disable-next-line global-require
|
|
26
|
+
const mysql = require('mysql2/promise');
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line global-require
|
|
29
|
+
const config = require(path.join(__dirname, '..', 'config.js'));
|
|
30
|
+
|
|
31
|
+
instance = mysql.createPool({
|
|
32
|
+
host: config.DB_HOST,
|
|
33
|
+
database: config.DB_NAME,
|
|
34
|
+
user: config.DB_USER,
|
|
35
|
+
password: config.DB_PASS,
|
|
36
|
+
charset: 'utf8mb4',
|
|
37
|
+
waitForConnections: true,
|
|
38
|
+
connectionLimit: 10,
|
|
39
|
+
namedPlaceholders: false,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return instance;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = Database;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mirror of src/backend/example.config.php
|
|
5
|
+
*
|
|
6
|
+
* Versioned, secret-free template. On setup, copy this file to config.js and
|
|
7
|
+
* fill in real values. config.js is git-ignored and stays local.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
// Default key for protected endpoints that don't have a specific key in CUSTOM_ENDPOINT_KEYS
|
|
12
|
+
GENERAL_API_KEY: 'DEFAULT_KEY',
|
|
13
|
+
|
|
14
|
+
// If you want to restrict access to protected endpoints to specific clients, define custom keys per endpoint.
|
|
15
|
+
// For subfolder endpoints, use the relative path ('subfolder/endpoint')
|
|
16
|
+
CUSTOM_ENDPOINT_KEYS: {
|
|
17
|
+
'subfolder/example-protected': 'custom-key',
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
GENERAL_ALLOWED_ORIGINS: [
|
|
21
|
+
'*',
|
|
22
|
+
// 'https://example.com',
|
|
23
|
+
],
|
|
24
|
+
|
|
25
|
+
CUSTOM_ENDPOINT_ORIGINS: {
|
|
26
|
+
'subfolder/example-protected': ['https://app.example.com'],
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Database configuration
|
|
30
|
+
DB_HOST: '127.0.0.1',
|
|
31
|
+
DB_NAME: 'example_db',
|
|
32
|
+
DB_USER: 'root',
|
|
33
|
+
DB_PASS: '',
|
|
34
|
+
|
|
35
|
+
// Environment: 'production' hides error details; anything else = debug.
|
|
36
|
+
APP_ENV: 'production',
|
|
37
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backend-node",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Node.js backend for a Nibula site. Coexists with the PHP backend; pick one at scaffold time. Front controller: _core/index.js.",
|
|
6
|
+
"main": "_core/index.js",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "node _core/index.js",
|
|
10
|
+
"dev": "APP_ENV=development node _core/index.js"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"mysql2": "^3.11.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/frontend/.htaccess
CHANGED
|
@@ -1,16 +1,51 @@
|
|
|
1
|
-
Options -Indexes
|
|
2
|
-
ErrorDocument 404 /404.html
|
|
3
|
-
ErrorDocument 403 /404.html
|
|
4
|
-
|
|
5
|
-
<IfModule mod_rewrite.c>
|
|
6
|
-
RewriteEngine On
|
|
7
|
-
RewriteBase /
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
Options -Indexes
|
|
2
|
+
ErrorDocument 404 /404.html
|
|
3
|
+
ErrorDocument 403 /404.html
|
|
4
|
+
|
|
5
|
+
<IfModule mod_rewrite.c>
|
|
6
|
+
RewriteEngine On
|
|
7
|
+
RewriteBase /
|
|
8
|
+
|
|
9
|
+
# Block sensitive files (web.config, dotfiles)
|
|
10
|
+
RewriteRule ^(web\.config|\..*)$ /404.html [L,NC]
|
|
11
|
+
</IfModule>
|
|
12
|
+
|
|
13
|
+
# === /api routing — covers BOTH backends ====================================
|
|
14
|
+
#
|
|
15
|
+
# Apache (unlike nginx) cannot health-check an upstream, so it cannot auto-fall
|
|
16
|
+
# back from Node to PHP. The behaviour is:
|
|
17
|
+
#
|
|
18
|
+
# - mod_proxy available -> /api is proxied to the Node backend (127.0.0.1:3000)
|
|
19
|
+
# - mod_proxy NOT present -> /api is rewritten to the PHP front controller
|
|
20
|
+
#
|
|
21
|
+
# Typical shared hosting (Aruba, cPanel) has no mod_proxy, so it uses PHP
|
|
22
|
+
# automatically. On a VPS running Node, mod_proxy sends /api to Node.
|
|
23
|
+
#
|
|
24
|
+
# If you are on a server that HAS mod_proxy but you want to use PHP, comment out
|
|
25
|
+
# the mod_proxy block below.
|
|
26
|
+
|
|
27
|
+
# --- Node backend (if mod_proxy is available) ---
|
|
28
|
+
<IfModule mod_proxy.c>
|
|
29
|
+
ProxyPreserveHost On
|
|
30
|
+
ProxyPass /api/ http://127.0.0.1:3000/api/ retry=0
|
|
31
|
+
ProxyPassReverse /api/ http://127.0.0.1:3000/api/
|
|
32
|
+
|
|
33
|
+
<IfModule mod_headers.c>
|
|
34
|
+
RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s"
|
|
35
|
+
RequestHeader set X-Forwarded-Proto "https"
|
|
36
|
+
</IfModule>
|
|
37
|
+
</IfModule>
|
|
38
|
+
|
|
39
|
+
# --- PHP backend (fallback when mod_proxy is absent) ---
|
|
40
|
+
<IfModule mod_rewrite.c>
|
|
41
|
+
RewriteEngine On
|
|
42
|
+
RewriteBase /
|
|
43
|
+
RewriteRule ^api/(.*)$ backend/_core/index.php [QSA,L]
|
|
44
|
+
</IfModule>
|
|
45
|
+
|
|
46
|
+
# === Static files ===========================================================
|
|
47
|
+
<IfModule mod_rewrite.c>
|
|
48
|
+
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
|
49
|
+
RewriteCond %{REQUEST_FILENAME} -d
|
|
50
|
+
RewriteRule ^ - [L]
|
|
51
|
+
</IfModule>
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
{
|
|
2
|
-
"site_name": "Site name",
|
|
3
|
-
"title": "Site title",
|
|
4
|
-
"description": "Site description",
|
|
5
|
-
"keywords": "keyword1, keyword2, keyword3",
|
|
6
|
-
"domain": "yoursite.com",
|
|
7
|
-
"url": "https://yoursite.com",
|
|
8
|
-
"lang": "en",
|
|
9
|
-
"author": "Name and surname",
|
|
10
|
-
"data_bs_theme": "dark",
|
|
11
|
-
"theme_color": {
|
|
12
|
-
"light": "#ffffff",
|
|
13
|
-
"dark": "#0d1117"
|
|
14
|
-
},
|
|
15
|
-
"favicon": "/assets/brand/favicon.svg",
|
|
16
|
-
"logo": "/assets/brand/logo.svg",
|
|
17
|
-
"legal": {
|
|
18
|
-
"privacy": "",
|
|
19
|
-
"cookie": "",
|
|
20
|
-
"cookieControls": "",
|
|
21
|
-
"terms": "",
|
|
22
|
-
"copyright": {
|
|
23
|
-
"year": "2026",
|
|
24
|
-
"text": "All rights reserved."
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
"pages": {
|
|
28
|
-
"404": {
|
|
29
|
-
"seo": {
|
|
30
|
-
"title": "404 - Not found",
|
|
31
|
-
"keywords": "",
|
|
32
|
-
"noindex": true,
|
|
33
|
-
"canonical": ""
|
|
34
|
-
},
|
|
35
|
-
"cdn": {
|
|
36
|
-
"css": [],
|
|
37
|
-
"js": []
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
"homepage": {
|
|
41
|
-
"seo": {
|
|
42
|
-
"title": "Homepage",
|
|
43
|
-
"description": "Description",
|
|
44
|
-
"keywords": "",
|
|
45
|
-
"noindex": false,
|
|
46
|
-
"canonical": ""
|
|
47
|
-
},
|
|
48
|
-
"cdn": {
|
|
49
|
-
"css": [],
|
|
50
|
-
"js": []
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"site_name": "Site name",
|
|
3
|
+
"title": "Site title",
|
|
4
|
+
"description": "Site description",
|
|
5
|
+
"keywords": "keyword1, keyword2, keyword3",
|
|
6
|
+
"domain": "yoursite.com",
|
|
7
|
+
"url": "https://yoursite.com",
|
|
8
|
+
"lang": "en",
|
|
9
|
+
"author": "Name and surname",
|
|
10
|
+
"data_bs_theme": "dark",
|
|
11
|
+
"theme_color": {
|
|
12
|
+
"light": "#ffffff",
|
|
13
|
+
"dark": "#0d1117"
|
|
14
|
+
},
|
|
15
|
+
"favicon": "/assets/brand/favicon.svg",
|
|
16
|
+
"logo": "/assets/brand/logo.svg",
|
|
17
|
+
"legal": {
|
|
18
|
+
"privacy": "",
|
|
19
|
+
"cookie": "",
|
|
20
|
+
"cookieControls": "",
|
|
21
|
+
"terms": "",
|
|
22
|
+
"copyright": {
|
|
23
|
+
"year": "2026",
|
|
24
|
+
"text": "All rights reserved."
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"pages": {
|
|
28
|
+
"404": {
|
|
29
|
+
"seo": {
|
|
30
|
+
"title": "404 - Not found",
|
|
31
|
+
"keywords": "",
|
|
32
|
+
"noindex": true,
|
|
33
|
+
"canonical": ""
|
|
34
|
+
},
|
|
35
|
+
"cdn": {
|
|
36
|
+
"css": [],
|
|
37
|
+
"js": []
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"homepage": {
|
|
41
|
+
"seo": {
|
|
42
|
+
"title": "Homepage",
|
|
43
|
+
"description": "Description",
|
|
44
|
+
"keywords": "",
|
|
45
|
+
"noindex": false,
|
|
46
|
+
"canonical": ""
|
|
47
|
+
},
|
|
48
|
+
"cdn": {
|
|
49
|
+
"css": [],
|
|
50
|
+
"js": []
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/frontend/web.config
CHANGED
|
@@ -1,27 +1,55 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
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".
|
|
16
|
+
-->
|
|
17
|
+
<configuration>
|
|
18
|
+
<system.webServer>
|
|
19
|
+
<rewrite>
|
|
20
|
+
<rules>
|
|
21
|
+
|
|
22
|
+
<!-- Block sensitive files -->
|
|
23
|
+
<rule name="BlockSensitive" stopProcessing="true">
|
|
24
|
+
<match url="^(web\.config|\..*)$" ignoreCase="true" />
|
|
25
|
+
<action type="CustomResponse" statusCode="404" />
|
|
26
|
+
</rule>
|
|
27
|
+
|
|
28
|
+
<!-- Node backend (requires ARR). Remove this rule to use PHP. -->
|
|
29
|
+
<rule name="ApiToNode" stopProcessing="true">
|
|
30
|
+
<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>
|
|
35
|
+
</rule>
|
|
36
|
+
|
|
37
|
+
<!-- PHP backend fallback: rewrite /api to the PHP front controller. -->
|
|
38
|
+
<rule name="ApiToPhp" stopProcessing="true">
|
|
39
|
+
<match url="^api/(.*)" ignoreCase="true" />
|
|
40
|
+
<action type="Rewrite" url="backend/_core/index.php" appendQueryString="true" />
|
|
41
|
+
</rule>
|
|
42
|
+
|
|
43
|
+
</rules>
|
|
44
|
+
</rewrite>
|
|
45
|
+
|
|
46
|
+
<directoryBrowse enabled="false" />
|
|
47
|
+
|
|
48
|
+
<httpErrors errorMode="Custom" existingResponse="Replace">
|
|
49
|
+
<remove statusCode="404" />
|
|
50
|
+
<remove statusCode="403" />
|
|
51
|
+
<error statusCode="404" path="/404.html" responseMode="ExecuteURL" />
|
|
52
|
+
<error statusCode="403" path="/404.html" responseMode="ExecuteURL" />
|
|
53
|
+
</httpErrors>
|
|
54
|
+
</system.webServer>
|
|
55
|
+
</configuration>
|