hlassiege/darylpublic Fork
⑂ edc162a
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/daryl.git
git clone ssh://git@rickub.com/hlassiege/daryl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat: serve the website locally without building it

hlassiege committed 2026-11-13T09:44:22+01:00 Browse files
edc162a parent: 139efc8
modified README.md +2 -1
@@ -272,7 +272,8 @@ ids in `web/scripts/screenshots.mjs`):
272272 cd web; pnpm screenshots
273273 ```
274274
275-The website lives in `site/`: `node site/build.mjs` builds it into `site/dist/`. It is deployed to
275+The website lives in `site/`. `node site/serve.mjs` serves it on http://localhost:4477 while you work on it
276+(reload the page after an edit); `node site/build.mjs` builds it into `site/dist/`. It is deployed to
276277 Bunny CDN by `.rickub/workflows/deploy-site.yml` when `site/`, the screenshots or the logo change.
277278
278279 ## Contributing
@@ -272,7 +272,8 @@ ids in `web/scripts/screenshots.mjs`):
272 cd web; pnpm screenshots272 cd web; pnpm screenshots
273 ```273 ```
274 274
275-The website lives in `site/`: `node site/build.mjs` builds it into `site/dist/`. It is deployed to275+The website lives in `site/`. `node site/serve.mjs` serves it on http://localhost:4477 while you work on it
276+(reload the page after an edit); `node site/build.mjs` builds it into `site/dist/`. It is deployed to
276 Bunny CDN by `.rickub/workflows/deploy-site.yml` when `site/`, the screenshots or the logo change.277 Bunny CDN by `.rickub/workflows/deploy-site.yml` when `site/`, the screenshots or the logo change.
277 278
278 ## Contributing279 ## Contributing
added site/serve.mjs +55 -0
new file mode 100644
@@ -0,0 +1,55 @@
1+// Serves the website for local work, without building it: site/public as is, the screenshots and the logo
2+// from their place in the repository (the same mapping as build.mjs). Edit a file, reload the page.
3+// No dependencies: `node site/serve.mjs [port]`, then open http://localhost:4477.
4+import { createReadStream } from 'node:fs'
5+import { stat } from 'node:fs/promises'
6+import { createServer } from 'node:http'
7+import { extname, join, normalize, sep } from 'node:path'
8+import { fileURLToPath } from 'node:url'
9+
10+const root = (path) => fileURLToPath(new URL(`../${path}`, import.meta.url))
11+const port = Number(process.argv[2] ?? process.env.PORT ?? 4477)
12+
13+/** URL prefix → folder or file, the first match wins */
14+const MOUNTS = [
15+ ['/screenshots/', root('media/screenshots/')],
16+ ['/favicon.svg', root('web/public/favicon.svg')],
17+ ['/', root('site/public/')],
18+]
19+
20+const TYPES = {
21+ '.html': 'text/html; charset=utf-8',
22+ '.css': 'text/css; charset=utf-8',
23+ '.js': 'text/javascript; charset=utf-8',
24+ '.svg': 'image/svg+xml',
25+ '.png': 'image/png',
26+ '.jpg': 'image/jpeg',
27+ '.webp': 'image/webp',
28+ '.woff2': 'font/woff2',
29+ '.txt': 'text/plain; charset=utf-8',
30+}
31+
32+function resolve(pathname) {
33+ for (const [prefix, target] of MOUNTS) {
34+ if (pathname === prefix) return target.endsWith(sep) ? join(target, 'index.html') : target
35+ if (prefix.endsWith('/') && pathname.startsWith(prefix)) {
36+ const file = normalize(join(target, pathname.slice(prefix.length)))
37+ return file.startsWith(target) ? file : null // no way out of the mount
38+ }
39+ }
40+ return null
41+}
42+
43+createServer(async (req, res) => {
44+ const { pathname } = new URL(req.url, 'http://localhost')
45+ let file = resolve(decodeURIComponent(pathname))
46+ try {
47+ if (file && (await stat(file)).isDirectory()) file = join(file, 'index.html')
48+ if (!file || !(await stat(file)).isFile()) throw new Error('not found')
49+ } catch {
50+ res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }).end('Not found')
51+ return
52+ }
53+ res.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream', 'cache-control': 'no-store' })
54+ createReadStream(file).pipe(res)
55+}).listen(port, () => console.log(`Site on http://localhost:${port}`))
new file mode 100644
@@ -0,0 +1,55 @@
1+// Serves the website for local work, without building it: site/public as is, the screenshots and the logo
2+// from their place in the repository (the same mapping as build.mjs). Edit a file, reload the page.
3+// No dependencies: `node site/serve.mjs [port]`, then open http://localhost:4477.
4+import { createReadStream } from 'node:fs'
5+import { stat } from 'node:fs/promises'
6+import { createServer } from 'node:http'
7+import { extname, join, normalize, sep } from 'node:path'
8+import { fileURLToPath } from 'node:url'
9+
10+const root = (path) => fileURLToPath(new URL(`../${path}`, import.meta.url))
11+const port = Number(process.argv[2] ?? process.env.PORT ?? 4477)
12+
13+/** URL prefix → folder or file, the first match wins */
14+const MOUNTS = [
15+ ['/screenshots/', root('media/screenshots/')],
16+ ['/favicon.svg', root('web/public/favicon.svg')],
17+ ['/', root('site/public/')],
18+]
19+
20+const TYPES = {
21+ '.html': 'text/html; charset=utf-8',
22+ '.css': 'text/css; charset=utf-8',
23+ '.js': 'text/javascript; charset=utf-8',
24+ '.svg': 'image/svg+xml',
25+ '.png': 'image/png',
26+ '.jpg': 'image/jpeg',
27+ '.webp': 'image/webp',
28+ '.woff2': 'font/woff2',
29+ '.txt': 'text/plain; charset=utf-8',
30+}
31+
32+function resolve(pathname) {
33+ for (const [prefix, target] of MOUNTS) {
34+ if (pathname === prefix) return target.endsWith(sep) ? join(target, 'index.html') : target
35+ if (prefix.endsWith('/') && pathname.startsWith(prefix)) {
36+ const file = normalize(join(target, pathname.slice(prefix.length)))
37+ return file.startsWith(target) ? file : null // no way out of the mount
38+ }
39+ }
40+ return null
41+}
42+
43+createServer(async (req, res) => {
44+ const { pathname } = new URL(req.url, 'http://localhost')
45+ let file = resolve(decodeURIComponent(pathname))
46+ try {
47+ if (file && (await stat(file)).isDirectory()) file = join(file, 'index.html')
48+ if (!file || !(await stat(file)).isFile()) throw new Error('not found')
49+ } catch {
50+ res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }).end('Not found')
51+ return
52+ }
53+ res.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream', 'cache-control': 'no-store' })
54+ createReadStream(file).pipe(res)
55+}).listen(port, () => console.log(`Site on http://localhost:${port}`))