-6
.example.env
-6
.example.env
+18
-19
README.md
+18
-19
README.md
···
3
3
A simple way to host a website via a tangled repo.
4
4
You can run it as a cloudflare worker or as an express server.
5
5
6
-
## Setup
6
+
## Run
7
7
8
-
Create .env pointing to the repo you want to host:
8
+
Create a `config.json` for your site(s).
9
9
10
+
```json
11
+
{
12
+
"site": {
13
+
"knotDomain": "knot.gracekind.net",
14
+
"ownerDid": "did:plc:p572wxnsuoogcrhlfrlizlrb",
15
+
"repoName": "tangled-pages-example",
16
+
"branch": "main",
17
+
"baseDir": "/public", // optional
18
+
"notFoundFilepath": "/404.html" // optional
19
+
}
20
+
}
10
21
```
11
-
KNOT_DOMAIN=knot.gracekind.net
12
-
OWNER_DID=did:plc:p572wxnsuoogcrhlfrlizlrb
13
-
REPO_NAME=tangled-pages-example
14
-
BRANCH=main
15
-
```
22
+
23
+
See `config.multiple.example.json` for an example of a multi-site config.
16
24
17
-
Run server:
25
+
Then run:
18
26
19
27
```bash
20
28
npm install
21
-
npm start
22
-
```
23
-
24
-
## Config
25
-
26
-
You can configure the site by creating a `pages_config.yaml` file in the root of the hosted repo.
27
-
28
-
```yaml
29
-
baseDir: "/public"
30
-
notFoundFilepath: "404.html"
29
+
npx tangled-pages --config config.json
31
30
```
32
31
33
32
## Limitations
34
33
35
-
It fetches files from the repo on request, so it might be slow.
34
+
The server fetches files from the repo on request, so it might be slow.
36
35
In the future, we could cache the files and use a CI to clear the cache as needed.
+11
config.example.json
+11
config.example.json
+15
config.multiple.example.json
+15
config.multiple.example.json
···
1
+
{
2
+
"sites": [
3
+
{
4
+
"subdomain": "tangled-pages-example",
5
+
"knotDomain": "knot.gracekind.net",
6
+
"ownerDid": "did:plc:p572wxnsuoogcrhlfrlizlrb",
7
+
"repoName": "tangled-pages-example",
8
+
"branch": "main",
9
+
"baseDir": "/public",
10
+
"notFoundFilepath": "/404.html"
11
+
}
12
+
],
13
+
"subdomainOffset": 1,
14
+
"subdomainOffset:comment": "Subdomain offset will usually be 1 for localhost, 2 for production"
15
+
}
+11
config.worker..example.json
+11
config.worker..example.json
+157
-21
package-lock.json
+157
-21
package-lock.json
···
8
8
"name": "tangled-pages",
9
9
"version": "1.0.0",
10
10
"dependencies": {
11
-
"dotenv": "^17.2.1",
12
11
"express": "^5.1.0",
13
12
"nodemon": "^3.1.10",
14
13
"wrangler": "^4.33.0",
15
-
"yaml": "^2.8.1"
14
+
"yargs": "^18.0.0"
15
+
},
16
+
"bin": {
17
+
"tangled-pages": "bin/tangled-pages.js"
16
18
}
17
19
},
18
20
"node_modules/@cloudflare/kv-asset-handler": {
···
1019
1021
"node": ">=0.4.0"
1020
1022
}
1021
1023
},
1024
+
"node_modules/ansi-regex": {
1025
+
"version": "6.2.0",
1026
+
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
1027
+
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
1028
+
"license": "MIT",
1029
+
"engines": {
1030
+
"node": ">=12"
1031
+
},
1032
+
"funding": {
1033
+
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
1034
+
}
1035
+
},
1036
+
"node_modules/ansi-styles": {
1037
+
"version": "6.2.1",
1038
+
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
1039
+
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
1040
+
"license": "MIT",
1041
+
"engines": {
1042
+
"node": ">=12"
1043
+
},
1044
+
"funding": {
1045
+
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
1046
+
}
1047
+
},
1022
1048
"node_modules/anymatch": {
1023
1049
"version": "3.1.3",
1024
1050
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
···
1156
1182
"fsevents": "~2.3.2"
1157
1183
}
1158
1184
},
1185
+
"node_modules/cliui": {
1186
+
"version": "9.0.1",
1187
+
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
1188
+
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
1189
+
"license": "ISC",
1190
+
"dependencies": {
1191
+
"string-width": "^7.2.0",
1192
+
"strip-ansi": "^7.1.0",
1193
+
"wrap-ansi": "^9.0.0"
1194
+
},
1195
+
"engines": {
1196
+
"node": ">=20"
1197
+
}
1198
+
},
1159
1199
"node_modules/color": {
1160
1200
"version": "4.2.3",
1161
1201
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
···
1277
1317
"node": ">=8"
1278
1318
}
1279
1319
},
1280
-
"node_modules/dotenv": {
1281
-
"version": "17.2.1",
1282
-
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
1283
-
"integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
1284
-
"license": "BSD-2-Clause",
1285
-
"engines": {
1286
-
"node": ">=12"
1287
-
},
1288
-
"funding": {
1289
-
"url": "https://dotenvx.com"
1290
-
}
1291
-
},
1292
1320
"node_modules/dunder-proto": {
1293
1321
"version": "1.0.1",
1294
1322
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
···
1306
1334
"version": "1.1.1",
1307
1335
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
1308
1336
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
1337
+
},
1338
+
"node_modules/emoji-regex": {
1339
+
"version": "10.4.0",
1340
+
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
1341
+
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
1342
+
"license": "MIT"
1309
1343
},
1310
1344
"node_modules/encodeurl": {
1311
1345
"version": "2.0.0",
···
1389
1423
"@esbuild/win32-arm64": "0.25.4",
1390
1424
"@esbuild/win32-ia32": "0.25.4",
1391
1425
"@esbuild/win32-x64": "0.25.4"
1426
+
}
1427
+
},
1428
+
"node_modules/escalade": {
1429
+
"version": "3.2.0",
1430
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1431
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1432
+
"license": "MIT",
1433
+
"engines": {
1434
+
"node": ">=6"
1392
1435
}
1393
1436
},
1394
1437
"node_modules/escape-html": {
···
1529
1572
"url": "https://github.com/sponsors/ljharb"
1530
1573
}
1531
1574
},
1575
+
"node_modules/get-caller-file": {
1576
+
"version": "2.0.5",
1577
+
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
1578
+
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
1579
+
"license": "ISC",
1580
+
"engines": {
1581
+
"node": "6.* || 8.* || >= 10.*"
1582
+
}
1583
+
},
1584
+
"node_modules/get-east-asian-width": {
1585
+
"version": "1.3.0",
1586
+
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
1587
+
"integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
1588
+
"license": "MIT",
1589
+
"engines": {
1590
+
"node": ">=18"
1591
+
},
1592
+
"funding": {
1593
+
"url": "https://github.com/sponsors/sindresorhus"
1594
+
}
1595
+
},
1532
1596
"node_modules/get-intrinsic": {
1533
1597
"version": "1.3.0",
1534
1598
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
···
2258
2322
"npm": ">=6"
2259
2323
}
2260
2324
},
2325
+
"node_modules/string-width": {
2326
+
"version": "7.2.0",
2327
+
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
2328
+
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
2329
+
"license": "MIT",
2330
+
"dependencies": {
2331
+
"emoji-regex": "^10.3.0",
2332
+
"get-east-asian-width": "^1.0.0",
2333
+
"strip-ansi": "^7.1.0"
2334
+
},
2335
+
"engines": {
2336
+
"node": ">=18"
2337
+
},
2338
+
"funding": {
2339
+
"url": "https://github.com/sponsors/sindresorhus"
2340
+
}
2341
+
},
2342
+
"node_modules/strip-ansi": {
2343
+
"version": "7.1.0",
2344
+
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
2345
+
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
2346
+
"license": "MIT",
2347
+
"dependencies": {
2348
+
"ansi-regex": "^6.0.1"
2349
+
},
2350
+
"engines": {
2351
+
"node": ">=12"
2352
+
},
2353
+
"funding": {
2354
+
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
2355
+
}
2356
+
},
2261
2357
"node_modules/supports-color": {
2262
2358
"version": "5.5.0",
2263
2359
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
···
2429
2525
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
2430
2526
"license": "MIT"
2431
2527
},
2528
+
"node_modules/wrap-ansi": {
2529
+
"version": "9.0.0",
2530
+
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
2531
+
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
2532
+
"license": "MIT",
2533
+
"dependencies": {
2534
+
"ansi-styles": "^6.2.1",
2535
+
"string-width": "^7.0.0",
2536
+
"strip-ansi": "^7.1.0"
2537
+
},
2538
+
"engines": {
2539
+
"node": ">=18"
2540
+
},
2541
+
"funding": {
2542
+
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
2543
+
}
2544
+
},
2432
2545
"node_modules/wrappy": {
2433
2546
"version": "1.0.2",
2434
2547
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
···
2455
2568
}
2456
2569
}
2457
2570
},
2458
-
"node_modules/yaml": {
2459
-
"version": "2.8.1",
2460
-
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
2461
-
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
2571
+
"node_modules/y18n": {
2572
+
"version": "5.0.8",
2573
+
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
2574
+
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
2462
2575
"license": "ISC",
2463
-
"bin": {
2464
-
"yaml": "bin.mjs"
2576
+
"engines": {
2577
+
"node": ">=10"
2578
+
}
2579
+
},
2580
+
"node_modules/yargs": {
2581
+
"version": "18.0.0",
2582
+
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
2583
+
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
2584
+
"license": "MIT",
2585
+
"dependencies": {
2586
+
"cliui": "^9.0.1",
2587
+
"escalade": "^3.1.1",
2588
+
"get-caller-file": "^2.0.5",
2589
+
"string-width": "^7.2.0",
2590
+
"y18n": "^5.0.5",
2591
+
"yargs-parser": "^22.0.0"
2465
2592
},
2466
2593
"engines": {
2467
-
"node": ">= 14.6"
2594
+
"node": "^20.19.0 || ^22.12.0 || >=23"
2595
+
}
2596
+
},
2597
+
"node_modules/yargs-parser": {
2598
+
"version": "22.0.0",
2599
+
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
2600
+
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
2601
+
"license": "ISC",
2602
+
"engines": {
2603
+
"node": "^20.19.0 || ^22.12.0 || >=23"
2468
2604
}
2469
2605
},
2470
2606
"node_modules/youch": {
+8
-6
package.json
+8
-6
package.json
···
2
2
"name": "tangled-pages",
3
3
"version": "1.0.0",
4
4
"type": "module",
5
+
"bin": {
6
+
"tangled-pages": "bin/tangled-pages.js"
7
+
},
5
8
"scripts": {
6
-
"start": "npm run dev",
7
-
"dev": "npm run dev:worker",
8
-
"dev:worker": "wrangler dev --port 3000 --env development",
9
-
"dev:express": "nodemon src/server.js"
9
+
"start": "npm run dev:express",
10
+
"dev": "npm run dev:express",
11
+
"dev:express": "nodemon src/server.js",
12
+
"dev:worker": "wrangler dev --port 3000"
10
13
},
11
14
"dependencies": {
12
-
"dotenv": "^17.2.1",
13
15
"express": "^5.1.0",
14
16
"nodemon": "^3.1.10",
15
17
"wrangler": "^4.33.0",
16
-
"yaml": "^2.8.1"
18
+
"yargs": "^18.0.0"
17
19
}
18
20
}
+8
-97
src/pages-service.js
+8
-97
src/pages-service.js
···
1
1
import { getContentTypeForExtension, trimLeadingSlash } from "./helpers.js";
2
2
import path from "node:path";
3
-
import yaml from "yaml";
4
3
5
4
// Helpers
6
5
···
9
8
return getContentTypeForExtension(extension);
10
9
}
11
10
12
-
class FileCache {
13
-
constructor({ expirationSeconds = 60 }) {
14
-
this.cache = new Map();
15
-
this.expirationSeconds = expirationSeconds;
16
-
}
17
-
18
-
get(filename) {
19
-
if (this.cache.has(filename)) {
20
-
const entry = this.cache.get(filename);
21
-
if (
22
-
entry &&
23
-
entry.timestamp > Date.now() - this.expirationSeconds * 1000
24
-
) {
25
-
return entry.content;
26
-
}
27
-
}
28
-
return null;
29
-
}
30
-
31
-
set(filename, content) {
32
-
const timestamp = Date.now();
33
-
this.cache.set(filename, { content, timestamp });
34
-
}
35
-
}
36
-
37
-
class PagesConfig {
38
-
constructor({ baseDir, notFoundFilepath }) {
39
-
this.baseDir = baseDir;
40
-
this.notFoundFilepath = notFoundFilepath;
41
-
}
42
-
43
-
static default() {
44
-
return new PagesConfig({
45
-
baseDir: "/",
46
-
notFoundFilepath: null,
47
-
});
48
-
}
49
-
50
-
static fromFile(filePath, fileContent) {
51
-
if (!filePath.endsWith(".yaml")) {
52
-
throw new Error("Config file must be a YAML file");
53
-
}
54
-
let configObj = {};
55
-
try {
56
-
configObj = yaml.parse(fileContent);
57
-
} catch (error) {
58
-
throw new Error(`Error parsing YAML file ${filePath}: ${error}`);
59
-
}
60
-
return new PagesConfig({
61
-
baseDir: configObj.baseDir || "/",
62
-
notFoundFilepath: configObj.notFoundFilepath || null,
63
-
});
64
-
}
65
-
}
66
-
67
11
class KnotClient {
68
12
constructor({ domain, ownerDid, repoName, branch }) {
69
13
this.domain = domain;
···
98
42
ownerDid,
99
43
repoName,
100
44
branch,
101
-
configFilepath = "pages_config.yaml",
102
-
fileCacheExpirationSeconds = 10,
45
+
baseDir = "/",
46
+
notFoundFilepath = null,
103
47
}) {
104
48
this.domain = domain;
105
49
this.ownerDid = ownerDid;
106
50
this.repoName = repoName;
107
51
this.branch = branch;
108
-
this.configFilepath = configFilepath;
109
-
this.fileCache = new FileCache({
110
-
expirationSeconds: fileCacheExpirationSeconds,
111
-
});
52
+
this.baseDir = baseDir;
53
+
this.notFoundFilepath = notFoundFilepath;
112
54
this.client = new KnotClient({
113
55
domain: domain,
114
56
ownerDid: ownerDid,
···
117
59
});
118
60
}
119
61
120
-
async getConfig() {
121
-
if (!this.__config) {
122
-
await this.loadConfig();
123
-
}
124
-
return this.__config;
125
-
}
126
-
127
-
async loadConfig() {
128
-
let config = null;
129
-
const configFileContent = await this.getFileContent(this.configFilepath);
130
-
if (!configFileContent) {
131
-
console.warn(
132
-
`No config file found at ${this.configFilepath}, using default config`
133
-
);
134
-
config = PagesConfig.default();
135
-
} else {
136
-
config = PagesConfig.fromFile(this.configFilepath, configFileContent);
137
-
}
138
-
this.__config = config;
139
-
return config;
140
-
}
141
-
142
62
async getFileContent(filename) {
143
-
const cachedContent = this.fileCache.get(filename);
144
-
if (cachedContent) {
145
-
return cachedContent;
146
-
}
147
63
let content = null;
148
64
const blob = await this.client.getBlob(filename);
149
65
if (blob.is_binary) {
···
151
67
} else {
152
68
content = blob.contents;
153
69
}
154
-
this.fileCache.set(filename, content);
155
70
return content;
156
71
}
157
72
158
73
async getPage(route) {
159
-
const config = await this.getConfig();
160
74
let filePath = route;
161
75
const extension = path.extname(filePath);
162
76
if (extension === "") {
163
77
filePath = path.join(filePath, "index.html");
164
78
}
165
-
166
-
const fullPath = path.join(config.baseDir, trimLeadingSlash(filePath));
167
-
79
+
const fullPath = path.join(this.baseDir, trimLeadingSlash(filePath));
168
80
const content = await this.getFileContent(fullPath);
169
81
if (!content) {
170
82
return this.get404();
···
177
89
}
178
90
179
91
async get404() {
180
-
const { notFoundFilepath } = await this.getConfig();
181
-
if (notFoundFilepath) {
182
-
const content = await this.getFileContent(notFoundFilepath);
92
+
if (this.notFoundFilepath) {
93
+
const content = await this.getFileContent(this.notFoundFilepath);
183
94
return {
184
95
status: 404,
185
96
content,
186
-
contentType: getContentTypeForFilename(notFoundFilepath),
97
+
contentType: getContentTypeForFilename(this.notFoundFilepath),
187
98
};
188
99
}
189
100
return { status: 404, content: "Not Found", contentType: "text/plain" };
+71
-24
src/server.js
+71
-24
src/server.js
···
1
1
import PagesService from "./pages-service.js";
2
2
import express from "express";
3
-
import dotenv from "dotenv";
3
+
import fs from "fs";
4
+
import yargs from "yargs";
4
5
5
-
dotenv.config();
6
+
class Config {
7
+
constructor({ site, sites, subdomainOffset }) {
8
+
this.site = site;
9
+
this.sites = sites || [];
10
+
this.subdomainOffset = subdomainOffset;
11
+
}
6
12
7
-
const pagesService = new PagesService({
8
-
domain: process.env.KNOT_DOMAIN,
9
-
ownerDid: process.env.OWNER_DID,
10
-
repoName: process.env.REPO_NAME,
11
-
branch: process.env.BRANCH,
12
-
verbose: process.env.NODE_ENV === "development",
13
-
});
13
+
static fromFile(filepath) {
14
+
const config = JSON.parse(fs.readFileSync(filepath, "utf8"));
15
+
return new Config(config);
16
+
}
17
+
}
14
18
15
-
// preload to make sure there are no problems with the config
16
-
await pagesService.loadConfig();
19
+
class Server {
20
+
constructor(config) {
21
+
this.config = config;
22
+
this.app = express();
17
23
18
-
const app = express();
24
+
if (config.subdomainOffset) {
25
+
this.app.set("subdomain offset", config.subdomainOffset);
26
+
}
27
+
28
+
this.app.get("/{*any}", async (req, res) => {
29
+
const subdomain = req.subdomains.at(-1);
30
+
// Single site mode
31
+
if (!subdomain) {
32
+
if (this.config.site) {
33
+
return this.handleSiteRequest(req, res, this.config.site);
34
+
} else {
35
+
return res.status(200).send("Tangled pages is running!");
36
+
}
37
+
}
38
+
// Multi site mode
39
+
const matchingSite = this.config.sites.find(
40
+
(site) => site.subdomain === subdomain
41
+
);
42
+
if (matchingSite) {
43
+
await this.handleSiteRequest(req, res, matchingSite);
44
+
} else {
45
+
console.log("No matching site found for subdomain", subdomain);
46
+
return res.status(404).send("Not Found");
47
+
}
48
+
});
49
+
}
19
50
20
-
app.get("/{*any}", async (req, res) => {
21
-
const route = req.path;
22
-
const { status, content, contentType } = await pagesService.getPage(route);
23
-
res.status(status).set("Content-Type", contentType).send(content);
24
-
});
51
+
async handleSiteRequest(req, res, site) {
52
+
const route = req.path;
53
+
const pagesService = new PagesService({
54
+
domain: site.knotDomain,
55
+
ownerDid: site.ownerDid,
56
+
repoName: site.repoName,
57
+
branch: site.branch,
58
+
baseDir: site.baseDir,
59
+
notFoundFilepath: site.notFoundFilepath,
60
+
});
61
+
const { status, content, contentType } = await pagesService.getPage(route);
62
+
res.status(status).set("Content-Type", contentType).send(content);
63
+
}
25
64
26
-
function main() {
27
-
const server = app.listen(3000, () => {
28
-
console.log("Server is running on port 3000");
29
-
});
65
+
async start() {
66
+
this.app.listen(3000, () => {
67
+
console.log("Server is running on port 3000");
68
+
});
69
+
this.app.on("error", (error) => {
70
+
console.error("Server error:", error);
71
+
});
72
+
}
73
+
}
30
74
31
-
server.on("error", (error) => {
32
-
console.error("Server error:", error);
33
-
});
75
+
async function main() {
76
+
const args = yargs(process.argv.slice(2)).parse();
77
+
const configFilepath = args.config || "config.json";
78
+
const config = Config.fromFile(configFilepath);
79
+
const server = new Server(config);
80
+
await server.start();
34
81
}
35
82
36
83
main();
+38
-23
src/worker.js
+38
-23
src/worker.js
···
1
1
import PagesService from "./pages-service.js";
2
+
import config from "../config.worker.example.json"; // must be set at build time
2
3
3
-
let pagesService = null;
4
-
5
-
// idk how long cloudflare keeps this around.
6
-
// it would be better to save the config in a KV store
7
-
// but this is good enough for now
8
-
function getPagesService(env) {
9
-
if (!pagesService) {
10
-
pagesService = new PagesService({
11
-
domain: env.KNOT_DOMAIN,
12
-
ownerDid: env.OWNER_DID,
13
-
repoName: env.REPO_NAME,
14
-
branch: env.BRANCH,
15
-
verbose: env.NODE_ENV === "development",
16
-
});
17
-
}
18
-
return pagesService;
4
+
async function handleSiteRequest(request, site) {
5
+
const url = new URL(request.url);
6
+
const host = url.host;
7
+
const route = url.pathname;
8
+
const pagesService = new PagesService({
9
+
domain: site.knotDomain,
10
+
ownerDid: site.ownerDid,
11
+
repoName: site.repoName,
12
+
branch: site.branch,
13
+
baseDir: site.baseDir,
14
+
notFoundFilepath: site.notFoundFilepath,
15
+
});
16
+
const { status, content, contentType } = await pagesService.getPage(route);
17
+
return new Response(content, {
18
+
status,
19
+
headers: { "Content-Type": contentType },
20
+
});
19
21
}
20
22
21
23
export default {
22
24
async fetch(request, env, ctx) {
23
-
const route = new URL(request.url).pathname;
24
-
const pagesService = getPagesService(env);
25
-
const { status, content, contentType } = await pagesService.getPage(route);
26
-
return new Response(content, {
27
-
status,
28
-
headers: { "Content-Type": contentType },
29
-
});
25
+
const url = new URL(request.url);
26
+
const host = url.host;
27
+
const subdomainOffset = config.subdomainOffset ?? 2;
28
+
const subdomain = host.split(".").at((subdomainOffset + 1) * -1);
29
+
// Single site mode
30
+
if (!subdomain) {
31
+
if (config.site) {
32
+
return handleSiteRequest(request, config.site);
33
+
} else {
34
+
return new Response("Tangled pages is running!", { status: 200 });
35
+
}
36
+
}
37
+
// Multi site mode
38
+
const matchingSite = config.sites.find(
39
+
(site) => site.subdomain === subdomain
40
+
);
41
+
if (matchingSite) {
42
+
return handleSiteRequest(request, matchingSite);
43
+
}
44
+
return new Response("Not Found", { status: 404 });
30
45
},
31
46
};