Copy Build an HTTP cache-policy simulator with Tailwind CSS. Include controls for public or private caching, max-age, stale-while-revalidate, and immutable behavior. Show the generated Cache-Control header, a request-age slider, response outcome badge, browser and CDN decision steps, and a horizontal freshness timeline. Add JavaScript that recalculates HIT, STALE, REVALIDATE, or MISS behavior from the policy, updates the header, and explains each decision in plain language. <div class="mx-auto max-w-4xl p-6"><section class="overflow-hidden rounded-2xl border border-neutral-200 bg-white shadow-sm"><header class="border-b border-neutral-100 p-5"><h2 class="text-lg font-semibold text-neutral-900">Cache policy simulator</h2><p class="mt-1 text-sm text-neutral-500">See how a response behaves as it ages.</p></header><div class="grid md:grid-cols-[0.8fr_1.2fr]"><div class="space-y-5 border-b p-5 md:border-b-0 md:border-r"><label class="block text-xs font-medium text-neutral-600">Visibility<select id="cacheVisibility" class="mt-1.5 w-full rounded-xl border border-neutral-200 px-3 py-2.5 text-sm"><option value="public">Public · CDN and browser</option><option value="private">Private · browser only</option></select></label><label class="block text-xs font-medium text-neutral-600">Fresh for <span id="freshLabel" class="float-right">300s</span><input id="freshRange" type="range" min="0" max="3600" step="60" value="300" class="mt-3 w-full accent-neutral-900" /></label><label class="block text-xs font-medium text-neutral-600">Serve stale for <span id="staleLabel" class="float-right">600s</span><input id="staleRange" type="range" min="0" max="3600" step="60" value="600" class="mt-3 w-full accent-neutral-900" /></label><label class="flex items-center justify-between rounded-xl border border-neutral-200 p-3 text-sm"><span>Immutable asset</span><input id="cacheImmutable" type="checkbox" /></label><div><p class="text-xs font-medium text-neutral-600">Response age <span id="ageLabel" class="float-right">240s</span></p><input id="ageRange" type="range" min="0" max="7200" step="60" value="240" class="mt-3 w-full accent-violet-600" /></div></div><div class="p-5"><div class="rounded-xl bg-neutral-950 p-4"><p class="text-[10px] uppercase tracking-wider text-white/40">Response header</p><code id="cacheHeader" class="mt-2 block break-all text-sm text-cyan-200"></code></div><div class="mt-6 flex items-center justify-between"><div><p class="text-xs text-neutral-400">Decision at current age</p><h3 id="cacheDecision" class="mt-1 text-xl font-semibold"></h3></div><span id="cacheBadge" class="rounded-full px-3 py-1.5 text-xs font-medium"></span></div><div class="mt-5 h-3 overflow-hidden rounded-full bg-neutral-100"><span id="freshSegment" class="inline-block h-full bg-green-400"></span><span id="staleSegment" class="inline-block h-full bg-amber-300"></span></div><div class="mt-2 flex justify-between text-[10px] text-neutral-400"><span>Fresh</span><span>Stale while revalidating</span><span>Expired</span></div><ol id="cacheSteps" class="mt-6 space-y-2"></ol></div></div></section></div> var visibility = document.getElementById("cacheVisibility"); var fresh = document.getElementById("freshRange"); var stale = document.getElementById("staleRange"); var age = document.getElementById("ageRange"); var immutable = document.getElementById("cacheImmutable"); function renderCache() { var freshSeconds = Number(fresh.value); var staleSeconds = Number(stale.value); var ageSeconds = Number(age.value); var header = visibility.value + ", max-age=" + freshSeconds + (staleSeconds ? ", stale-while-revalidate=" + staleSeconds : "") + (immutable.checked ? ", immutable" : ""); document.getElementById("cacheHeader").textContent = "Cache-Control: " + header; document.getElementById("freshLabel").textContent = freshSeconds + "s"; document.getElementById("staleLabel").textContent = staleSeconds + "s"; document.getElementById("ageLabel").textContent = ageSeconds + "s"; var state = ageSeconds <= freshSeconds ? "HIT" : ageSeconds <= freshSeconds + staleSeconds ? "STALE" : "MISS"; if (immutable.checked && ageSeconds <= freshSeconds) state = "IMMUTABLE HIT"; document.getElementById("cacheDecision").textContent = state === "HIT" ? "Serve cached response" : state === "STALE" ? "Serve stale, refresh behind it" : state === "IMMUTABLE HIT" ? "Reuse without revalidation" : "Fetch from origin"; var badge = document.getElementById("cacheBadge"); badge.textContent = state; badge.className = state.indexOf("HIT") !== -1 ? "rounded-full bg-green-50 px-3 py-1.5 text-xs font-medium text-green-700" : state === "STALE" ? "rounded-full bg-amber-50 px-3 py-1.5 text-xs font-medium text-amber-700" : "rounded-full bg-red-50 px-3 py-1.5 text-xs font-medium text-red-700"; var total = Math.max(1, freshSeconds + staleSeconds); document.getElementById("freshSegment").style.width = freshSeconds / total * 100 + "%"; document.getElementById("staleSegment").style.width = staleSeconds / total * 100 + "%"; var steps = state === "STALE" ? ["Browser finds an aged cached response.", "Response is served immediately.", "Revalidation runs in the background."] : state.indexOf("HIT") !== -1 ? ["Cache key matches the request.", "Stored response is still fresh.", "No origin request is needed."] : ["Stored response is outside its stale window.", "Request continues to the origin.", "New response replaces the expired entry."]; document.getElementById("cacheSteps").innerHTML = steps.map(function (step, index) { return '<li class="flex gap-3 rounded-xl bg-neutral-50 p-3 text-sm text-neutral-600"><span class="text-neutral-300">0' + (index + 1) + '</span>' + step + '</li>'; }).join(""); } [visibility, fresh, stale, age, immutable].forEach(function (control) { control.addEventListener("input", renderCache); }); renderCache();