← back to build-pages
initial scaffold — build-pages live git-as-DB doc site (port 9700, Butlr first)
cd73726fad005a1afcef546cde9fe9b871431870 · 2026-05-13 11:29:53 -0700 · SteveStudio2
Files touched
A .gitignoreA package-lock.jsonA package.jsonA public/css/site.cssA server.js
Diff
commit cd73726fad005a1afcef546cde9fe9b871431870
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 13 11:29:53 2026 -0700
initial scaffold — build-pages live git-as-DB doc site (port 9700, Butlr first)
---
.gitignore | 9 +
package-lock.json | 928 ++++++++++++++++++++++++++++++++++++++++++++++++++++
package.json | 16 +
public/css/site.css | 114 +++++++
server.js | 319 ++++++++++++++++++
5 files changed, 1386 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4a0b3ea
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+.env
+.env.*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..3e03507
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,928 @@
+{
+ "name": "build-pages",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "build-pages",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "compression": "^1.8.1",
+ "express": "^5.2.1"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
+ "debug": "2.6.9",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.1.0",
+ "safe-buffer": "5.2.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/compression/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/compression/node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.0.0",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1e9c4fb
--- /dev/null
+++ b/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "build-pages",
+ "version": "1.0.0",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "description": "",
+ "dependencies": {
+ "compression": "^1.8.1",
+ "express": "^5.2.1"
+ }
+}
diff --git a/public/css/site.css b/public/css/site.css
new file mode 100644
index 0000000..7064d77
--- /dev/null
+++ b/public/css/site.css
@@ -0,0 +1,114 @@
+:root {
+ --bg: #faf9f6;
+ --fg: #1a1a1a;
+ --muted: #6b6b6b;
+ --border: #e3e0d8;
+ --code-bg: #f0ede4;
+ --link: #6b4d1a;
+ --link-hov: #c9a14b;
+ --accent: #ffd633;
+ --chip-bg: #ede9dd;
+}
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #0d0c0a;
+ --fg: #ece8db;
+ --muted: #8a8576;
+ --border: #2a2620;
+ --code-bg: #1a1814;
+ --link: #d4b46a;
+ --link-hov: #ffd633;
+ --accent: #ffd633;
+ --chip-bg: #1f1c15;
+ }
+}
+* { box-sizing: border-box; }
+html, body { margin: 0; padding: 0; }
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 1.55;
+ background: var(--bg);
+ color: var(--fg);
+ -webkit-font-smoothing: antialiased;
+}
+a { color: var(--link); text-decoration: none; }
+a:hover { color: var(--link-hov); text-decoration: underline; }
+code, pre {
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+ font-size: 13px;
+}
+pre { background: var(--code-bg); padding: 14px 18px; overflow-x: auto; border-radius: 6px; }
+.bp-hdr {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 18px 28px; border-bottom: 1px solid var(--border);
+ background: var(--bg);
+ position: sticky; top: 0; z-index: 10;
+}
+.bp-logo { font-weight: 800; font-size: 18px; letter-spacing: -0.5px; color: var(--fg); }
+.bp-logo:hover { color: var(--link-hov); text-decoration: none; }
+.bp-nav a { margin-left: 18px; color: var(--muted); font-size: 14px; }
+.bp-main {
+ max-width: 1100px;
+ margin: 0 auto;
+ padding: 32px 28px 80px;
+}
+.bp-ftr {
+ text-align: center; padding: 22px; color: var(--muted); font-size: 13px;
+ border-top: 1px solid var(--border);
+}
+h1 { font-size: 32px; font-weight: 700; margin: 0 0 6px; letter-spacing: -0.7px; }
+h2 { font-size: 20px; font-weight: 700; margin: 36px 0 14px; letter-spacing: -0.3px; }
+.bp-lede { font-size: 17px; color: var(--muted); margin: 0 0 8px; max-width: 720px; }
+.bp-meta { color: var(--muted); font-size: 14px; margin: 4px 0 18px; }
+.bp-cards { list-style: none; padding: 0; display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
+.bp-card {
+ border: 1px solid var(--border); border-radius: 10px; padding: 18px 20px;
+ background: var(--code-bg);
+}
+.bp-card a h2 { margin: 0 0 6px; color: var(--fg); }
+.bp-card a:hover h2 { color: var(--link-hov); }
+.bp-card p { margin: 4px 0; color: var(--muted); font-size: 14px; }
+.bp-section { margin-top: 38px; }
+.bp-q {
+ width: 100%; padding: 10px 14px; font-size: 15px;
+ border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--fg);
+}
+.bp-q:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
+.bp-commits { list-style: none; padding: 0; margin: 14px 0; }
+.bp-commits li {
+ display: grid; grid-template-columns: 80px 100px 1fr; gap: 12px; align-items: baseline;
+ padding: 8px 12px; border-bottom: 1px solid var(--border);
+ font-size: 14px;
+}
+.bp-commits li:hover { background: var(--code-bg); }
+.bp-commits code { background: var(--code-bg); padding: 2px 6px; border-radius: 4px; }
+.bp-d { color: var(--muted); font-variant-numeric: tabular-nums; }
+.bp-s { color: var(--fg); }
+.bp-grid { display: grid; gap: 28px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
+.bp-chips { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
+.bp-chips li {
+ display: inline-flex; align-items: center; gap: 6px;
+ padding: 4px 10px; background: var(--chip-bg); border-radius: 999px;
+ font-size: 13px;
+}
+.bp-chips li span { color: var(--fg); }
+.bp-chips li b { color: var(--muted); font-weight: 600; }
+.bp-idea {
+ border: 1px solid var(--border); border-radius: 6px; margin: 10px 0;
+ padding: 10px 14px; background: var(--code-bg);
+}
+.bp-idea summary { cursor: pointer; font-size: 14px; }
+.bp-idea pre { background: transparent; padding: 10px 0 0; font-size: 13px; white-space: pre-wrap; }
+.bp-h { color: var(--link); font-weight: 700; }
+.bp-files { columns: 2; column-gap: 28px; list-style: none; padding: 0; font-size: 13px; }
+.bp-files li { break-inside: avoid; padding: 2px 0; }
+.bp-files code { background: transparent; }
+.bp-body { margin: 20px 0; }
+.bp-body pre { background: var(--code-bg); white-space: pre-wrap; }
+.bp-diff { font-size: 12px; max-height: 70vh; overflow: auto; }
+.bp-source { font-size: 12px; max-height: 80vh; overflow: auto; white-space: pre; }
+@media (max-width: 640px) {
+ .bp-commits li { grid-template-columns: 1fr; gap: 2px; }
+ .bp-files { columns: 1; }
+}
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..2d6be10
--- /dev/null
+++ b/server.js
@@ -0,0 +1,319 @@
+// build-pages — a live build-journal site. Every project Steve ships gets a
+// page surfacing the full git history with full diffs, file tree, message
+// search, and auto-extracted "ideas / agents / skills" parsed from commit
+// bodies. No DB — git is the source of truth, queried on demand.
+
+const express = require('express');
+const compression = require('compression');
+const path = require('path');
+const fs = require('fs');
+const { execFile } = require('child_process');
+const { promisify } = require('util');
+const exec = promisify(execFile);
+
+const PORT = parseInt(process.env.PORT || '9700', 10);
+const HOME = process.env.HOME;
+
+// Registry: kebab-slug → {name, repo, blurb}. Add a project by appending here.
+// Slug is the URL path /p/<slug>; repo is an absolute path to the working tree.
+const PROJECTS = {
+ butlr: {
+ name: 'Butlr',
+ repo: path.join(HOME, 'Projects/holdforme'),
+ blurb: 'We wait on hold so you don\'t have to. Twilio Voice + Whisper transcripts + optional AI agent.',
+ aliases: ['holdforme'],
+ },
+};
+
+// ─────────────────────────────────────────────────────────────────────────────
+// git helpers — narrow, single-purpose, all run with cwd=repo. Each returns a
+// JSON-friendly shape so the routes are just glue.
+// ─────────────────────────────────────────────────────────────────────────────
+async function gitRun(repo, args) {
+ const { stdout } = await exec('git', args, { cwd: repo, maxBuffer: 50 * 1024 * 1024 });
+ return stdout;
+}
+
+async function listCommits(repo) {
+ const fmt = '%H%x01%h%x01%ai%x01%an%x01%s%x01%b%x02';
+ const raw = await gitRun(repo, ['log', '--no-merges', `--pretty=format:${fmt}`]);
+ return raw
+ .split('\x02\n')
+ .map(s => s.trim())
+ .filter(Boolean)
+ .map(line => {
+ const [hash, shortHash, dateISO, author, subject, body = ''] = line.split('\x01');
+ return { hash, shortHash, dateISO, author, subject, body: body.replace(/\x02$/, '') };
+ });
+}
+
+async function commitDetail(repo, hash) {
+ const log = await gitRun(repo, ['log', '-1', '--pretty=format:%H%n%ai%n%an%n%s%n%n%b', hash]);
+ const lines = log.split('\n');
+ const out = {
+ hash: lines[0],
+ dateISO: lines[1],
+ author: lines[2],
+ subject: lines[3],
+ body: lines.slice(5).join('\n').trim(),
+ };
+ out.diff = await gitRun(repo, ['show', '--no-color', '--stat', '--patch', hash]);
+ out.files = (await gitRun(repo, ['show', '--name-status', '--pretty=format:', hash])).trim().split('\n').filter(Boolean);
+ return out;
+}
+
+async function listFiles(repo) {
+ const raw = await gitRun(repo, ['ls-files']);
+ return raw.split('\n').filter(Boolean);
+}
+
+async function readBlob(repo, ref, file) {
+ return gitRun(repo, ['show', `${ref}:${file}`]);
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Idea / agent / skill extraction. Long commit bodies on Steve's projects often
+// embed the design rationale + which sub-agent or skill was used. Parse those
+// out so the doc page surfaces them as first-class facets.
+// ─────────────────────────────────────────────────────────────────────────────
+function extractFacets(commits) {
+ const agentRe = /\b(claude-code-guide|domain-name-agent|lawyer-build-agent|doctor-agent|secrets-manager|exa-agent|search-specialist|technical-researcher|website-analysis|debate-team-fast|abramstasks|prompt-engineer|backend-architect|frontend-developer|code-reviewer|security-auditor|debugger|database-architect|test-engineer|ai-engineer|architect-reviewer|deployment-engineer|comms-compliance|vp-engineering|vp-operations|vp-dw-commerce|vp-compliance-policy|vp-directories|vp-research-content)\b/gi;
+ const skillRe = /\/([a-z][a-z0-9-]{2,40})\b/g;
+ const agents = new Map();
+ const skills = new Map();
+ const ideas = [];
+ for (const c of commits) {
+ const blob = `${c.subject}\n${c.body}`;
+ for (const m of blob.matchAll(agentRe)) {
+ const a = m[1].toLowerCase();
+ agents.set(a, (agents.get(a) || 0) + 1);
+ }
+ for (const m of blob.matchAll(skillRe)) {
+ const s = m[1];
+ // Filter out path-y false positives — we want skill names like /loop /secrets, not /tmp /api /lib
+ if (/^(tmp|api|var|usr|etc|root|home|opt|public|src|lib|node|bin|dist|build|test|tests|css|js|img|files|email|sms|admin)$/.test(s)) continue;
+ skills.set(s, (skills.get(s) || 0) + 1);
+ }
+ // "creative idea" = body lines beginning with a numbered list, dash, or
+ // containing words like "idea/perfect/polish/scaffold" — surface them.
+ if (c.body.length > 120) {
+ ideas.push({ hash: c.shortHash, dateISO: c.dateISO, subject: c.subject, body: c.body });
+ }
+ }
+ const sort = (m) => [...m.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ({ name: k, count: v }));
+ return { agents: sort(agents), skills: sort(skills), ideas };
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// HTML shell + escaping. The page is static-rendered server-side, then a small
+// inline script wires up the client-side search filter (no build step).
+// ─────────────────────────────────────────────────────────────────────────────
+function esc(s) {
+ return String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
+}
+
+function shell(title, body, headExtra = '') {
+ return `<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>${esc(title)}</title>
+ <meta name="color-scheme" content="light dark">
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%23111'/><text x='16' y='22' font-family='Georgia,serif' font-size='20' fill='%23ffd633' text-anchor='middle'>BP</text></svg>">
+ <link rel="stylesheet" href="/css/site.css">
+ ${headExtra}
+</head>
+<body>
+ <header class="bp-hdr">
+ <a class="bp-logo" href="/">build-pages</a>
+ <nav class="bp-nav">
+ <a href="/">all projects</a>
+ </nav>
+ </header>
+ <main class="bp-main">
+ ${body}
+ </main>
+ <footer class="bp-ftr">
+ <span>git is the source of truth — this page is rendered live from <code>git log</code>.</span>
+ </footer>
+</body>
+</html>`;
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Routes
+// ─────────────────────────────────────────────────────────────────────────────
+const app = express();
+app.disable('x-powered-by');
+app.use(compression());
+app.use((req, res, next) => {
+ res.setHeader('X-Content-Type-Options', 'nosniff');
+ res.setHeader('X-Frame-Options', 'SAMEORIGIN');
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
+ next();
+});
+app.use('/css', express.static(path.join(__dirname, 'public/css'), { maxAge: '1h' }));
+
+app.get('/healthz', (_req, res) => {
+ res.setHeader('Cache-Control', 'no-store');
+ res.json({ ok: true, ts: Date.now() });
+});
+
+// Index — all projects.
+app.get('/', (_req, res) => {
+ const rows = Object.entries(PROJECTS).map(([slug, p]) => {
+ let count = 0, latest = '';
+ try {
+ const out = require('child_process').execSync('git rev-list --count HEAD', { cwd: p.repo }).toString().trim();
+ count = parseInt(out, 10);
+ latest = require('child_process').execSync('git log -1 --pretty=%ai', { cwd: p.repo }).toString().trim();
+ } catch (_) {}
+ return `<li class="bp-card">
+ <a href="/p/${esc(slug)}"><h2>${esc(p.name)}</h2></a>
+ <p>${esc(p.blurb)}</p>
+ <p class="bp-meta">${count} commits · latest ${esc(latest.slice(0, 10))}</p>
+ </li>`;
+ }).join('\n');
+ res.send(shell('build-pages — all projects', `
+ <h1>build journals</h1>
+ <p class="bp-lede">Every build, every commit, every line. Click into a project to see how it actually got made.</p>
+ <ul class="bp-cards">${rows}</ul>
+ `));
+});
+
+// Project page — commits + facets + search + file tree.
+app.get('/p/:slug', async (req, res, next) => {
+ const p = PROJECTS[req.params.slug];
+ if (!p) return res.status(404).send(shell('Not found', '<h1>404</h1><p>No such project.</p>'));
+ try {
+ const commits = await listCommits(p.repo);
+ const facets = extractFacets(commits);
+ const files = await listFiles(p.repo);
+ const idxData = JSON.stringify(commits.map(c => ({
+ h: c.shortHash, d: c.dateISO.slice(0, 10), s: c.subject, b: c.body.slice(0, 4000),
+ })));
+ const ideasHtml = facets.ideas.slice(0, 50).map(i => `
+ <details class="bp-idea">
+ <summary><span class="bp-h">${esc(i.hash)}</span> · <span class="bp-d">${esc(i.dateISO.slice(0, 10))}</span> · ${esc(i.subject)}</summary>
+ <pre>${esc(i.body)}</pre>
+ </details>`).join('\n');
+ const fileLis = files.slice(0, 500).map(f => `<li><a href="/p/${esc(req.params.slug)}/file?path=${encodeURIComponent(f)}">${esc(f)}</a></li>`).join('');
+ res.send(shell(`${p.name} — build journal`, `
+ <h1>${esc(p.name)}</h1>
+ <p class="bp-lede">${esc(p.blurb)}</p>
+ <p class="bp-meta">repo: <code>${esc(p.repo.replace(HOME, '~'))}</code> · ${commits.length} commits</p>
+
+ <section class="bp-section">
+ <h2>Search the build</h2>
+ <input id="q" class="bp-q" type="search" placeholder="filter commits by subject, body, file…" autocomplete="off">
+ <p id="bp-tally" class="bp-meta">${commits.length} commits indexed</p>
+ <ol id="bp-commits" class="bp-commits">
+ ${commits.map(c => `
+ <li data-h="${esc(c.shortHash)}" data-blob="${esc((c.subject + ' ' + c.body).toLowerCase().slice(0, 1500))}">
+ <a href="/p/${esc(req.params.slug)}/c/${esc(c.shortHash)}"><code>${esc(c.shortHash)}</code></a>
+ <span class="bp-d">${esc(c.dateISO.slice(0, 10))}</span>
+ <span class="bp-s">${esc(c.subject)}</span>
+ </li>`).join('')}
+ </ol>
+ </section>
+
+ <section class="bp-section bp-grid">
+ <div>
+ <h2>Agents used</h2>
+ <ul class="bp-chips">
+ ${facets.agents.map(a => `<li><span>${esc(a.name)}</span><b>${a.count}</b></li>`).join('') || '<li class="bp-meta">none detected</li>'}
+ </ul>
+ </div>
+ <div>
+ <h2>Skills used</h2>
+ <ul class="bp-chips">
+ ${facets.skills.slice(0, 30).map(s => `<li><span>/${esc(s.name)}</span><b>${s.count}</b></li>`).join('') || '<li class="bp-meta">none detected</li>'}
+ </ul>
+ </div>
+ </section>
+
+ <section class="bp-section">
+ <h2>Creative ideas + design notes</h2>
+ <p class="bp-meta">Commits with substantial prose (≥120 chars) — the rationale behind each move.</p>
+ ${ideasHtml}
+ </section>
+
+ <section class="bp-section">
+ <h2>File tree</h2>
+ <p class="bp-meta">${files.length} files tracked. Click any to browse the source at HEAD.</p>
+ <ul class="bp-files">${fileLis}</ul>
+ </section>
+ `, `
+ <script>
+ const DATA = ${idxData};
+ const q = document.getElementById('q');
+ const list = document.getElementById('bp-commits');
+ const tally = document.getElementById('bp-tally');
+ q.addEventListener('input', () => {
+ const term = q.value.toLowerCase().trim();
+ let shown = 0;
+ for (const li of list.children) {
+ const blob = li.dataset.blob;
+ const match = !term || blob.includes(term);
+ li.style.display = match ? '' : 'none';
+ if (match) shown++;
+ }
+ tally.textContent = term ? \`\${shown} / \${DATA.length} match "\${q.value}"\` : \`\${DATA.length} commits indexed\`;
+ });
+ </script>
+ `));
+ } catch (e) { next(e); }
+});
+
+// Commit detail — full diff + body.
+app.get('/p/:slug/c/:hash', async (req, res, next) => {
+ const p = PROJECTS[req.params.slug];
+ if (!p) return res.status(404).send(shell('Not found', '<h1>404</h1>'));
+ try {
+ const c = await commitDetail(p.repo, req.params.hash);
+ res.send(shell(`${c.subject} — ${p.name}`, `
+ <p class="bp-meta"><a href="/p/${esc(req.params.slug)}">← back to ${esc(p.name)}</a></p>
+ <h1>${esc(c.subject)}</h1>
+ <p class="bp-meta"><code>${esc(c.hash)}</code> · ${esc(c.dateISO)} · ${esc(c.author)}</p>
+ ${c.body ? `<section class="bp-body"><pre>${esc(c.body)}</pre></section>` : ''}
+ <h2>Files touched</h2>
+ <ul class="bp-files">${c.files.map(f => `<li><code>${esc(f)}</code></li>`).join('')}</ul>
+ <h2>Diff</h2>
+ <pre class="bp-diff">${esc(c.diff)}</pre>
+ `));
+ } catch (e) { next(e); }
+});
+
+// File viewer — show any tracked path at HEAD.
+app.get('/p/:slug/file', async (req, res, next) => {
+ const p = PROJECTS[req.params.slug];
+ if (!p) return res.status(404).send(shell('Not found', '<h1>404</h1>'));
+ const file = req.query.path;
+ if (!file || file.includes('..')) return res.status(400).send(shell('Bad path', '<h1>400</h1>'));
+ try {
+ const content = await readBlob(p.repo, 'HEAD', file);
+ res.send(shell(`${file} — ${p.name}`, `
+ <p class="bp-meta"><a href="/p/${esc(req.params.slug)}">← back to ${esc(p.name)}</a></p>
+ <h1><code>${esc(file)}</code></h1>
+ <p class="bp-meta">${content.split('\n').length} lines</p>
+ <pre class="bp-source">${esc(content)}</pre>
+ `));
+ } catch (e) { next(e); }
+});
+
+// JSON API — for the eventual cross-project dashboard.
+app.get('/api/projects', (_req, res) => {
+ res.json({
+ projects: Object.entries(PROJECTS).map(([slug, p]) => ({ slug, name: p.name, repo: p.repo, blurb: p.blurb })),
+ });
+});
+
+app.use((err, _req, res, _next) => {
+ console.error('[build-pages]', err.message);
+ res.status(500).send(shell('Error', `<h1>500</h1><pre>${esc(err.message)}</pre>`));
+});
+
+app.listen(PORT, '127.0.0.1', () => {
+ console.log(`build-pages listening on http://127.0.0.1:${PORT}`);
+});
(oldest)
·
back to build-pages
·
api: add /api/health + /api/version (joins fleet-status dige 27818e5 →