Static site hosting via tangled

Enable multi site

-6
.example.env
··· 1 - # fill in and rename to .env 2 - # note these aren't secrets, just config 3 - KNOT_DOMAIN = "knot.gracekind.net" 4 - OWNER_DID = "did:plc:p572wxnsuoogcrhlfrlizlrb" 5 - REPO_NAME = "tangled-pages-example" 6 - BRANCH = "main"
+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.
+3
bin/tangled-pages.js
··· 1 + #!/usr/bin/env node 2 + 3 + import "../src/server.js";
+11
config.example.json
··· 1 + { 2 + "site": { 3 + "name": "tangled-pages-example", 4 + "knotDomain": "knot.gracekind.net", 5 + "ownerDid": "did:plc:p572wxnsuoogcrhlfrlizlrb", 6 + "repoName": "tangled-pages-example", 7 + "branch": "main", 8 + "baseDir": "/public", 9 + "notFoundFilepath": "/404.html" 10 + } 11 + }
+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
··· 1 + { 2 + "site": { 3 + "name": "tangled-pages-example", 4 + "knotDomain": "knot.gracekind.net", 5 + "ownerDid": "did:plc:p572wxnsuoogcrhlfrlizlrb", 6 + "repoName": "tangled-pages-example", 7 + "branch": "main", 8 + "baseDir": "/public", 9 + "notFoundFilepath": "/404.html" 10 + } 11 + }
+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
··· 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
··· 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
··· 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
··· 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 };