Reworked to be in Vite

This commit is contained in:
HangerThem 2026-06-18 10:21:50 +02:00
parent d686066339
commit 74fd3f25ee
25 changed files with 1623 additions and 336 deletions

View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Delayed Response | Web Under the Hood</title>
</head>
<body>
<h1>
Delayed Response
<!--BLINK-->
</h1>
<p>This response was delayed by 5 seconds.</p>
</body>
</html>

View file

@ -0,0 +1,8 @@
<h1>
Server received your POST
<!--BLINK-->
</h1>
<pre><!--LOGIN_DATA--></pre>
<p class="alert">
⚠️ Over plain HTTP, anyone between you and the server could read this.
</p>

View file

@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Form | Web Under the Hood</title>
</head>
<body>
<div id="form-data">
<h1>
Login Form
<!--BLINK-->
</h1>
<form id="login-form" class="flex flex-col gap-4 max-w-sm">
<label>Username: <input name="username" type="text" /></label>
<label>Password: <input name="password" type="password" /></label>
<button type="submit">Login</button>
</form>
<p>
Submit this and watch the terminal. Also check DevTools → Network tab.
</p>
<script type="module" src="/src/form.ts"></script>
</div>
</body>
</html>

View file

@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Under the Hood</title>
</head>
<body>
<h1>
HTTP Echo Server
<!--BLINK-->
</h1>
<p>Check your terminal - the server printed everything it received.</p>
<h2>Try these:</h2>
<ul>
<li><a href="/form">POST form demo</a></li>
<li><a href="/set-cookie">Set a cookie</a></li>
<li><a href="/show-cookies">Show cookies the server received</a></li>
<li><a href="/json">JSON response</a></li>
<li><a href="/delay">Delayed response</a></li>
<li><a href="/status">HTTP status codes</a></li>
</ul>
</body>
</html>

View file

@ -1,23 +1,23 @@
{ {
"name": "week-01-the_web_under_the_hood-demo", "name": "dem",
"version": "1.0.0", "private": true,
"description": "", "version": "0.0.0",
"main": "server.ts", "type": "module",
"scripts": { "scripts": {
"dev": "tsx watch server.ts", "dev": "tsx watch server.ts",
"build": "tsc server.ts --outDir dist", "build": "tsc server.ts --outDir dist",
"start": "node dist/server.js" "start": "node dist/server.js"
}, },
"keywords": [], "devDependencies": {
"author": "", "@tailwindcss/vite": "^4.3.1",
"license": "ISC", "@types/express": "^5.0.6",
"packageManager": "pnpm@10.22.0", "tailwindcss": "^4.3.1",
"tsx": "^4.22.4",
"typescript": "~6.0.2",
"vite": "^8.0.12"
},
"dependencies": { "dependencies": {
"express": "^5.2.1", "express": "^5.2.1",
"http-status": "^2.1.0" "http-status": "^2.1.0"
},
"devDependencies": {
"@types/express": "^5.0.6",
"tsx": "^4.22.3"
} }
} }

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -1,76 +0,0 @@
* {
font-family: "Courier New", Courier, monospace;
box-sizing: border-box;
}
body {
background-color: #040b04;
color: #7cff7c;
text-shadow: 0 0 5px rgba(124, 255, 124, 0.35);
line-height: 1.5;
margin: 0;
padding: 24px;
}
h1 {
color: #a8ff9f;
text-shadow: 0 0 8px rgba(124, 255, 124, 0.55);
}
p {
color: #7cff7c;
font-size: 18px;
}
a {
color: #9fffa0;
text-decoration: underline;
}
a:hover {
color: #d6ffd6;
}
ul {
list-style-type: square;
padding-left: 24px;
}
::selection {
background: rgba(124, 255, 124, 0.25);
color: #ffffff;
}
form {
margin-top: 16px;
max-width: 400px;
}
label {
display: flex;
flex-direction: column;
margin-bottom: 12px;
}
input {
padding: 8px;
border: 1px solid #7cff7c;
background-color: transparent;
color: #7cff7c;
font-size: 16px;
outline: none;
}
button {
padding: 10px 16px;
background-color: #7cff7c;
color: #040b04;
border: none;
cursor: pointer;
font-size: 16px;
font-weight: bold;
}
button:hover {
background-color: #9fffa0;
}

View file

@ -1,8 +1,17 @@
import express from "express" import express from "express"
import fs from "fs/promises"
import { status } from "http-status" import { status } from "http-status"
import { createServer as createViteServer } from "vite"
import { injectIntoHtml } from "./src/utils/inject"
import { partials } from "./src/plugins/injectPartials"
const app = express() const app = express()
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "mpa",
})
app.use(express.urlencoded({ extended: true })) app.use(express.urlencoded({ extended: true }))
app.use(express.json()) app.use(express.json())
@ -24,52 +33,22 @@ app.use((req, res, next) => {
next() next()
}) })
// Serve static files from the "public" directory (for CSS, images, etc.)
app.use(express.static("public"))
// Simple GET: echoes request info back as HTML
app.get("/", (req, res) => {
res.send(`
<link rel="stylesheet" href="/style.css" />
<h1>HTTP Echo Server</h1>
<p>Check your terminal the server printed everything it received.</p>
<h2>Try these:</h2>
<ul>
<li><a href="/form">POST form demo</a></li>
<li><a href="/set-cookie">Set a cookie</a></li>
<li><a href="/show-cookies">Show cookies the server received</a></li>
<li><a href="/json">JSON response</a></li>
<li><a href="/delay">Delayed response</a></li>
<li><a href="/status">HTTP status codes</a></li>
</ul>
`)
})
// Form page: allows to submit a POST and see the body
app.get("/form", (req, res) => {
res.send(`
<link rel="stylesheet" href="/style.css" />
<h1>Login Form</h1>
<form method="POST" action="/login">
<label>Username: <input name="username" type="text" /></label>
<label>Password: <input name="password" type="password" /></label>
<button type="submit">Login</button>
</form>
<p>Submit this and watch the terminal. Also check DevTools Network tab.</p>
`)
})
// Receives the POST: echoes the body back // Receives the POST: echoes the body back
app.post("/login", (req, res) => { app.post("/login", async (req, res) => {
const { username, password } = req.body const { username, password } = req.body
res.send(` const base = await fs.readFile("src/partials/form-response.html", "utf-8")
<link rel="stylesheet" href="/style.css" />
<h1>Server received your POST</h1> let result = await injectIntoHtml(base, [
<p><strong>Username:</strong> ${username}</p> ...partials,
<p><strong>Password:</strong> ${password}</p> {
<p style="color:red"> Over plain HTTP, anyone between you and the server could read this.</p> key: "<!--LOGIN_DATA-->",
<a href="/form">Back</a> placement: "replace",
`) type: "string",
value: JSON.stringify({ username, password }, null, 2),
},
])
res.send(result)
}) })
// Sets a cookie: shows Set-Cookie header in DevTools // Sets a cookie: shows Set-Cookie header in DevTools
@ -78,23 +57,25 @@ app.get("/set-cookie", (req, res) => {
httpOnly: true, httpOnly: true,
maxAge: 60 * 60 * 1000, // 1 hour maxAge: 60 * 60 * 1000, // 1 hour
}) })
res.send(` req.next?.()
<link rel="stylesheet" href="/style.css" />
<h1>Cookie Set</h1>
<p>The server sent a <code>Set-Cookie</code> header. Check DevTools Network Response Headers.</p>
<p>Now visit <a href="/show-cookies">show-cookies</a> the browser will automatically send it back.</p>
`)
}) })
// Shows what cookies the server received // Shows what cookies the server received
app.get("/show-cookies", (req, res) => { app.get("/show-cookies", async (req, res) => {
const cookieHeader = req.headers["cookie"] || "none" const cookieHeader = req.headers["cookie"] || "none"
res.send(` const base = await fs.readFile("show-cookie.html", "utf-8")
<link rel="stylesheet" href="/style.css" />
<h1>Cookies the server received</h1> let result = await injectIntoHtml(base, [
<p><code>Cookie: ${cookieHeader}</code></p> ...partials,
<p>The browser sent this automatically you didn't write any JavaScript for that.</p> {
`) key: "<!--COOKIES-->",
placement: "replace",
type: "string",
value: cookieHeader,
},
])
res.send(result)
}) })
// Returns JSON: shows Content-Type in action // Returns JSON: shows Content-Type in action
@ -110,68 +91,44 @@ app.get("/json", (req, res) => {
// Delayed response: simulates a slow server // Delayed response: simulates a slow server
app.get("/delay", (req, res) => { app.get("/delay", (req, res) => {
setTimeout(() => { setTimeout(() => {
res.send(` req.next?.()
<link rel="stylesheet" href="/style.css" />
<h1>Delayed Response</h1><p>This response was delayed by 5 seconds.</p>
`)
}, 5000) }, 5000)
}) })
// Show all the different HTTP status codes // Demonstrates different status codes
app.get("/status", (req, res) => { app.get("/status/:code", async (req, res) => {
res.send(` const code = parseInt(req.params.code)
<link rel="stylesheet" href="/style.css" /> const base = await fs.readFile("status-code.html", "utf-8")
<h1>HTTP Status Codes</h1>
<h2>2xx Success</h2> let result = await injectIntoHtml(base, [
<ul> ...partials,
${[200, 201, 202, 203, 204, 205, 206, 207, 208, 226] {
.map( key: "<!--HTTP_STATUS_CODE-->",
(code) => placement: "replace",
`<li><a href="/status/${code}">${code} ${status[code as keyof typeof status]}</a></li>`, type: "string",
) value: code.toString(),
.join("")} occurrence: "all",
</ul> },
<h2>3xx Redirection</h2> {
<ul> key: "<!--HTTP_STATUS_NAME-->",
${[300, 301, 302, 303, 304, 307, 308] placement: "replace",
.map( type: "string",
(code) => value: String(status[code as keyof typeof status]),
`<li><a href="/status/${code}">${code} ${status[code as keyof typeof status]}</a></li>`, occurrence: "all",
) },
.join("")} {
</ul> key: "<!--HTTP_HEADERS-->",
<h2>4xx Client Errors</h2> placement: "replace",
<ul> type: "string",
${[ value: JSON.stringify(req.headers, null, 2),
400, 401, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, occurrence: "all",
415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451, },
] ])
.map(
(code) => res.status(code).send(result)
`<li><a href="/status/${code}">${code} ${status[code as keyof typeof status]}</a></li>`,
)
.join("")}
</ul>
<h2>5xx Server Errors</h2>
<ul>
${[500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511]
.map(
(code) =>
`<li><a href="/status/${code}">${code} ${status[code as keyof typeof status]}</a></li>`,
)
.join("")}
</ul>
`)
}) })
// Demonstrates different status codes app.use(vite.middlewares)
app.get("/status/:code", (req, res) => {
const code = parseInt(req.params.code)
res.status(code).send(`
<link rel="stylesheet" href="/style.css" />
<h1>Status ${code}</h1><p>This response was sent with HTTP status ${code} (${status[code as keyof typeof status]}).</p>
`)
})
app.listen(3000, () => { app.listen(3000, () => {
console.log("Echo server running on http://localhost:3000") console.log("Echo server running on http://localhost:3000")

View file

@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Set Cookie | Web Under the Hood</title>
</head>
<body>
<h1>
Cookie Set
<!--BLINK-->
</h1>
<p>
The server sent a <code>Set-Cookie</code> header. Check DevTools → Network
→ Response Headers.
</p>
<p>
Now visit <a href="/show-cookies">show-cookies</a> — the browser will
automatically send it back.
</p>
</body>
</html>

View file

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Show Cookie | Web Under the Hood</title>
</head>
<body>
<h1>
Show Cookies
<!--BLINK-->
</h1>
<p>The server received the following cookies from the browser:</p>
<pre><!--COOKIES--></pre>
</body>
</html>

View file

@ -0,0 +1,25 @@
document
.querySelector("#login-form")!
.addEventListener("submit", async (event) => {
event.preventDefault()
const form = event.currentTarget as HTMLFormElement
const formData = new FormData(form)
const data = Object.fromEntries(formData.entries())
console.log("Submitting form with data:", data)
const response = await fetch("/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
if (!response.ok) {
console.error("Login failed:", response.status, response.statusText)
return
}
const resultHtml = await response.text()
document.querySelector("#form-data")!.innerHTML = resultHtml
})

View file

@ -0,0 +1 @@
<span class="blink"></span>

View file

@ -0,0 +1,8 @@
<h1>
Server received your POST
<!--BLINK-->
</h1>
<pre><!--LOGIN_DATA--></pre>
<p class="alert">
⚠️ Over plain HTTP, anyone between you and the server could read this.
</p>

View file

@ -0,0 +1,11 @@
<nav>
<ul class="flex gap-4">
<li><a href="/">Home</a></li>
<li><a href="/form">Form</a></li>
<li><a href="/set-cookie">Set Cookie</a></li>
<li><a href="/show-cookies">Show cookies</a></li>
<li><a href="/json">JSON</a></li>
<li><a href="/delay">Delayed</a></li>
<li><a href="/status">HTTP Status codes</a></li>
</ul>
</nav>

View file

@ -0,0 +1,48 @@
import type { Plugin } from "vite"
import { injectIntoHtml, type Injection } from "../utils/inject"
export function injectPartials(injections: Injection[]): Plugin {
return {
name: "inject-partials",
transformIndexHtml(html) {
return injectIntoHtml(html, injections, {
onMissing: (key, occurrence) =>
this.warn(
`[inject-partials] Marker "${key}" (occurrence: ${occurrence}) not found — skipping.`,
),
})
},
}
}
export const partials: Injection[] = [
{
key: "<body>",
placement: "after",
type: "html",
path: "src/partials/navigation.html",
optional: true,
},
{
key: "</head>",
placement: "before",
type: "string",
value: "<link rel='stylesheet' href='/src/style.css' />",
optional: true,
},
{
key: "<!--BLINK-->",
placement: "replace",
type: "html",
path: "src/partials/blink.html",
occurrence: "all",
optional: true,
},
{
key: "</head>",
placement: "before",
type: "string",
value: "<link rel='icon' type='image/svg+xml' href='/favicon.svg' />",
optional: true,
},
]

View file

@ -0,0 +1,54 @@
import { status } from "http-status"
document.querySelector("#http-200")!.innerHTML = [
200, 201, 202, 203, 204, 205, 206, 207, 208, 226,
]
.map(
(code) =>
`<li><a href="/status/${code}">${code} ${status[code as keyof typeof status]}</a></li>`,
)
.join("")
document.querySelector("#http-300")!.innerHTML = [
300, 301, 302, 303, 304, 307, 308,
]
.map(
(code) => `
<li>
<a href="/status/${code}"
>${code} ${status[code as keyof typeof status]}</a
>
</li>
`,
)
.join("")
document.querySelector("#http-400")!.innerHTML = [
400, 401, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415,
416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451,
]
.map(
(code) => `
<li>
<a href="/status/${code}"
>${code} ${status[code as keyof typeof status]}</a
>
</li>
`,
)
.join("")
document.querySelector("#http-500")!.innerHTML = [
500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511,
]
.map(
(code) =>
`
<li>
<a href="/status/${code}"
>${code} ${status[code as keyof typeof status]}</a
>
</li>
`,
)
.join("")

View file

@ -0,0 +1,165 @@
@import url("https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap");
@import "tailwindcss";
@keyframes flicker {
0%,
95%,
100% {
opacity: 1;
}
96% {
opacity: 0.85;
}
97% {
opacity: 1;
}
98% {
opacity: 0.9;
}
99% {
opacity: 1;
}
}
@keyframes scanline {
from {
transform: translateY(-100%);
}
to {
transform: translateY(100vh);
}
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
* {
@apply box-border leading-[1.85] font-normal tracking-[0.04em];
@apply selection:bg-lime-800/40 selection:text-lime-200;
font-family: "Share Tech Mono", "Courier New", monospace;
color: #5abf00;
text-shadow: 0 0 5px rgba(90, 191, 0, 0.3);
}
body {
@apply relative p-6 bg-[#0a0a00];
}
/* scanlines */
body::before {
@apply content-[''] fixed inset-0 pointer-events-none z-[9999];
background: repeating-linear-gradient(
0deg,
transparent 0px,
transparent 2px,
rgba(0, 0, 0, 0.18) 2px,
rgba(0, 0, 0, 0.18) 4px
);
}
/* vignette */
body::after {
@apply content-[''] fixed inset-0 pointer-events-none z-[9998];
background: radial-gradient(
ellipse at center,
transparent 55%,
rgba(0, 0, 0, 0.65) 100%
);
}
h1 {
@apply text-4xl font-normal tracking-[0.08em];
color: #a8ff40;
text-shadow:
0 0 10px rgba(140, 255, 60, 0.55),
0 0 22px rgba(140, 255, 60, 0.2);
animation: flicker 8s infinite;
}
h2 {
@apply text-xl font-normal tracking-[0.06em] border-b border-[#1a2a00] pb-1.5 mt-8;
color: #7dff40;
text-shadow: 0 0 8px rgba(120, 255, 60, 0.45);
}
a {
@apply border-b border-[#4a7a00] no-underline transition-colors duration-100;
color: #a8ff40;
text-shadow: 0 0 6px rgba(140, 255, 60, 0.4);
}
a:hover {
@apply border-[#8abf00];
color: #d4ff80;
}
ul {
@apply list-none pl-0;
}
ul li::before {
@apply content-['//\00a0'];
color: #2a4a00;
}
pre {
@apply bg-[#030300] border border-[#1a2a00] border-l-[3px] border-l-[#4a7a00] px-5 py-4 overflow-x-auto text-sm leading-[1.75];
color: #5abf00;
}
button {
@apply relative bg-transparent border border-[#4a7a00] px-5 py-2 text-xs tracking-[0.12em] uppercase cursor-pointer transition-all duration-100;
font-family: "Share Tech Mono", monospace;
color: #a8ff40;
text-shadow: 0 0 6px rgba(140, 255, 60, 0.4);
box-shadow:
inset 0 0 8px rgba(100, 200, 0, 0.05),
0 0 6px rgba(100, 200, 0, 0.1);
}
button::before {
@apply content-['▶\00a0'] text-[0.7rem];
color: #4a7a00;
}
button:hover {
box-shadow:
inset 0 0 12px rgba(100, 200, 0, 0.1),
0 0 10px rgba(100, 200, 0, 0.2);
background: rgba(100, 200, 0, 0.08);
border-color: #7dff40;
}
input {
@apply bg-transparent border-0 border-b border-[#2a4a00] pb-1 pt-1 text-sm outline-none transition-colors duration-150;
font-family: "Share Tech Mono", monospace;
color: #7dff40;
caret-color: #a8ff40;
text-shadow: 0 0 4px rgba(120, 255, 60, 0.3);
}
input:focus {
@apply border-[#7dff40];
}
input::placeholder {
@apply tracking-[0.1em];
color: #1a3000;
}
.blink {
animation: blink 1s step-end infinite;
}
.alert {
@apply text-sm mt-4 p-3 border border-[#7a7a00] bg-[#303000] rounded;
color: #ff9f40;
text-shadow: 0 0 4px rgba(255, 159, 64, 0.3);
}

View file

@ -0,0 +1,146 @@
import fs from "fs/promises"
import path from "path"
// ─── Types ────────────────────────────────────────────────────────────────────
export type Placement = "before" | "after" | "replace"
export type InjectionBase = {
key: string
placement: Placement
occurrence?: number | "all"
optional?: boolean
}
export type InjectionString = InjectionBase & {
type: "string"
value: string
}
export type InjectionHTML = InjectionBase & {
type: "html"
path: string
}
export type Injection = InjectionString | InjectionHTML
// ─── Helpers ──────────────────────────────────────────────────────────────────
function nthIndexOf(haystack: string, needle: string, n: number): number {
let index = -1
let remaining = n
let searchFrom = 0
while (remaining > 0) {
index = haystack.indexOf(needle, searchFrom)
if (index === -1) return -1
searchFrom = index + 1
remaining--
}
return index
}
function applyInjection(
html: string,
content: string,
markerIndex: number,
markerLength: number,
placement: Placement,
): string {
switch (placement) {
case "before":
return html.slice(0, markerIndex) + content + html.slice(markerIndex)
case "after": {
const after = markerIndex + markerLength
return html.slice(0, after) + content + html.slice(after)
}
case "replace":
return (
html.slice(0, markerIndex) +
content +
html.slice(markerIndex + markerLength)
)
}
}
// ─── Core ─────────────────────────────────────────────────────────────────────
export async function resolveContent(injection: Injection): Promise<string> {
if (injection.type === "html") {
try {
return await fs.readFile(path.resolve(injection.path), "utf-8")
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
throw new Error(`[inject] Failed to read "${injection.path}": ${message}`)
}
}
return injection.value
}
export type InjectOptions = {
/** Called when a marker is not found instead of throwing. Defaults to throwing. */
onMissing?: (key: string, occurrence: number | "all") => void
}
export async function injectIntoHtml(
html: string,
injections: Injection[],
options: InjectOptions = {},
): Promise<string> {
const contents = await Promise.all(injections.map(resolveContent))
for (let i = 0; i < injections.length; i++) {
const injection = injections[i]
const content = contents[i]
const occurrence = injection.occurrence ?? 1
const handleMissing = () => {
if (injection.optional) return
if (options.onMissing) {
options.onMissing(injection.key, occurrence)
} else {
throw new Error(
`[inject] Marker "${injection.key}" (occurrence: ${occurrence}) not found.`,
)
}
}
if (occurrence === "all") {
const indices: number[] = []
let searchFrom = 0
let idx: number
while ((idx = html.indexOf(injection.key, searchFrom)) !== -1) {
indices.push(idx)
searchFrom = idx + 1
}
if (indices.length === 0) {
handleMissing()
continue
}
for (let j = indices.length - 1; j >= 0; j--) {
html = applyInjection(
html,
content,
indices[j],
injection.key.length,
injection.placement,
)
}
} else {
const markerIndex = nthIndexOf(html, injection.key, occurrence)
if (markerIndex === -1) {
handleMissing()
continue
}
html = applyInjection(
html,
content,
markerIndex,
injection.key.length,
injection.placement,
)
}
}
return html
}

View file

@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><!--HTTP_STATUS_CODE--> | Web Under the Hood</title>
</head>
<body>
<h1>
Status
<!--HTTP_STATUS_CODE-->
<!--BLINK-->
</h1>
<p>
This response was sent with HTTP status
<!--HTTP_STATUS_CODE-->
(<!--HTTP_STATUS_NAME-->).
</p>
<h2>Headers</h2>
<pre><!--HTTP_HEADERS--></pre>
</body>
</html>

View file

@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HTTP Status Codes | Web Under the Hood</title>
</head>
<body>
<h1>
HTTP Status Codes
<!--BLINK-->
</h1>
<h2>2xx Success</h2>
<ul id="http-200"></ul>
<h2>3xx Redirection</h2>
<ul id="http-300"></ul>
<h2>4xx Client Errors</h2>
<ul id="http-400"></ul>
<h2>5xx Server Errors</h2>
<ul id="http-500"></ul>
<script type="module" src="/src/status.ts"></script>
</body>
</html>

View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "es2023",
"module": "esnext",
"lib": ["ES2023", "DOM"],
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View file

@ -0,0 +1,7 @@
import { defineConfig } from "vite"
import tailwindcss from "@tailwindcss/vite"
import { injectPartials, partials } from "./src/plugins/injectPartials"
export default defineConfig({
plugins: [tailwindcss(), injectPartials(partials)],
})