Routing and configuration
Connect the Link Vault page to its Worker
Build the Link Vault static interface and verify API routing, asset collisions, missing files, and browser error states.
We’ll add the Link Vault browser interface to the Worker from Reuse Hono routes. It will load saved links from the same origin as the page. The D1 lesson later makes those links persistent.
The Workers static assets guide covers general deployment, SPA fallbacks, and Astro. This exercise connects our course files and tests which layer answers each URL.
Attach the interface
Create public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Link Vault</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>Link Vault</h1>
<form id="save-link">
<label>Title <input name="title" required></label>
<label>URL <input name="url" type="url" required></label>
<button type="submit">Save link</button>
</form>
<button id="reload" type="button">Reload links</button>
<p id="status" role="status"></p>
<ul id="links"></ul>
<script type="module" src="/app.js"></script>
</body>
</html>
Create public/style.css with a readable baseline:
body {
max-width: 40rem;
margin: 2rem auto;
padding: 0 1rem;
font-family: system-ui, sans-serif;
}
label {
display: block;
margin-block: 1rem;
}
Merge this section into the generated wrangler.jsonc, keeping its entry point and existing bindings:
{
"assets": {
"directory": "./public",
"binding": "ASSETS",
"run_worker_first": ["/api/*"]
}
}
Files outside /api/* are served by the asset layer when they match. The API prefix reaches the Worker first, even if a file has the same path. Leave not_found_handling unset: Link Vault has no client-side router requiring an HTML fallback.
Connect the page to the API contract
At this stage, make your placeholder GET /api/links return an array with c.json([]). The browser code expects that shape. Keep the JSON app.notFound handler from the routing lesson.
Create public/app.js:
const form = document.querySelector('#save-link')
const reload = document.querySelector('#reload')
const status = document.querySelector('#status')
const list = document.querySelector('#links')
async function loadLinks() {
reload.disabled = true
list.replaceChildren()
status.textContent = 'Loading links…'
try {
const response = await fetch('/api/links')
if (!response.ok) throw new Error(`Load failed (HTTP ${response.status})`)
const links = await response.json()
if (!Array.isArray(links)) throw new Error('Expected a list of links')
for (const link of links) {
const url = new URL(link.url)
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('Unsupported link protocol')
}
const item = document.createElement('li')
const anchor = document.createElement('a')
anchor.href = url.href
anchor.textContent = link.title
item.append(anchor)
list.append(item)
}
status.textContent = links.length ? 'Links loaded' : 'No links saved yet'
} catch (error) {
list.replaceChildren()
status.textContent = error.message
} finally {
reload.disabled = false
}
}
form.addEventListener('submit', async event => {
event.preventDefault()
const button = form.querySelector('button')
const data = new FormData(form)
const url = new URL(data.get('url'))
if (!['http:', 'https:'].includes(url.protocol)) {
status.textContent = 'Use an HTTP or HTTPS URL'
return
}
button.disabled = true
status.textContent = 'Saving link…'
try {
const response = await fetch('/api/links', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: data.get('title'), url: url.href })
})
if (!response.ok) throw new Error(`Save failed (HTTP ${response.status})`)
form.reset()
await loadLinks()
} catch (error) {
status.textContent = error.message
} finally {
button.disabled = false
}
})
reload.addEventListener('click', loadLinks)
loadLinks()
A placeholder POST may return 201 without storing anything. The empty list after saving is expected until Store links in D1 replaces that placeholder. Keep validation on the server too; browser checks can be bypassed.
Prove the routing boundary
Run npm run dev and use the local URL Wrangler prints. With the usual port, check:
curl -i http://localhost:8787/
curl -i http://localhost:8787/api/health
curl -i http://localhost:8787/api/nothing
curl -i http://localhost:8787/app-old.js
curl -i http://localhost:8787/unknown-page
Expect HTML and 200 for /, JSON and 200 for health, JSON and 404 for the unknown API, and 404 for both missing browser paths. A missing script must never receive your HTML shell with 200.
Now create a temporary public/api/health file containing asset collision. Request /api/health again. It must still return the Worker’s JSON health response. Remove the temporary file afterward. This collision test proves /api/* is reserved for the API; an ordinary successful request doesn’t.
Use the browser states
Open the page and check its stylesheet and script in Network. Reload links with an empty response, then change the placeholder to return one link. Confirm both states work and the title renders as text.
Temporarily return 503 from the list route, then use DevTools Offline mode. Both should display an error and leave Reload enabled. Restore the route and network connection and retry. Submit a link once and inspect its JSON request body.
Check caching before deployment
HTML should refresh promptly because it names the current files. Only give files a long immutable cache lifetime after their filenames contain a content fingerprint. Our app.js and style.css don’t have one yet.
Shipping the page and scripts together also keeps rollbacks consistent. After a deployed rollback, load the page again and inspect the referenced script response. For custom static response headers, use Cloudflare’s static asset headers documentation.
Lesson completed