-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserve.mjs
More file actions
96 lines (87 loc) · 3.1 KB
/
Copy pathserve.mjs
File metadata and controls
96 lines (87 loc) · 3.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/**
* Minimal zero-dependency Node static server for the getprivacycode.com
* marketing site. Serves the pre-built export in ./site with clean-URL
* fallback (/cli -> cli.html).
*/
import { createServer } from "node:http"
import { createReadStream } from "node:fs"
import { stat } from "node:fs/promises"
import { fileURLToPath } from "node:url"
import { dirname, join, normalize, extname } from "node:path"
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "site")
const PORT = Number(process.env.PORT ?? 3000)
const MIME = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".ico": "image/x-icon",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".woff2": "font/woff2",
".woff": "font/woff",
".ttf": "font/ttf",
".txt": "text/plain; charset=utf-8",
".xml": "application/xml; charset=utf-8",
".webmanifest": "application/manifest+json",
}
async function exists(p) {
try {
const s = await stat(p)
return s.isFile()
} catch {
return false
}
}
// Resolve a request pathname to a file inside ROOT, guarding against traversal.
async function resolve(pathname) {
let p = decodeURIComponent(pathname)
if (p.endsWith("/")) p += "index.html"
const safe = normalize(p).replace(/^(\.\.[/\\])+/, "")
const base = join(ROOT, safe)
if (!base.startsWith(ROOT)) return null
const candidates = extname(base) ? [base] : [base, `${base}.html`, join(base, "index.html")]
for (const c of candidates) {
if (await exists(c)) return c
}
return null
}
const server = createServer(async (req, res) => {
try {
// Canonical host: redirect www.* -> apex over https, preserving path + query.
const host = (req.headers.host || "").toLowerCase()
if (host.startsWith("www.")) {
res.writeHead(301, { location: `https://${host.slice(4)}${req.url}` })
res.end()
return
}
const { pathname } = new URL(req.url, "http://localhost")
const match = (await resolve(pathname)) ?? (await resolve("/404.html"))
if (!match) {
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" })
res.end("Not Found")
return
}
const ext = extname(match)
const headers = { "content-type": MIME[ext] ?? "application/octet-stream" }
// Fingerprinted Next.js assets are safe to cache forever; HTML stays fresh.
if (pathname.startsWith("/_next/") || ext === ".woff2" || ext === ".woff") {
headers["cache-control"] = "public, max-age=31536000, immutable"
} else {
headers["cache-control"] = "public, max-age=0, must-revalidate"
}
res.writeHead(pathname === "/404.html" ? 404 : 200, headers)
createReadStream(match).pipe(res)
} catch (err) {
res.writeHead(500, { "content-type": "text/plain; charset=utf-8" })
res.end("Internal Server Error")
console.error(err)
}
})
server.listen(PORT, "0.0.0.0", () => {
console.log(`getprivacycode.com static site serving ./site on :${PORT}`)
})