<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Planet Performance</title>
<link>http://perfplanet.com/</link>
<description>News and views from the web performance blogosphere</description>
<language>en-us</language>
<pubDate>Sat, 10 Oct 2026 04:20:20 -0700</pubDate>
<lastBuildDate>Sat, 10 Oct 2026 04:20:20 -0700</lastBuildDate>
<atom:link href="http://perfplanet.com/rss.xml" rel="self" type="application/rss+xml" />
<item>
<title>Astro Tutorial: Make a Static Site with Interactive Components</title>
<link>https://www.debugbear.com/blog/astro-tutorial</link>
<guid isPermaLink="true">https://www.debugbear.com/blog/astro-tutorial</guid>
<description>This Astro tutorial shows how to build an interactive blog using file-based routing, build-time and live content collections, and the islands architecture.</description>
<pubDate>7 October 2026, 5:00 pm</pubDate>
<content:encoded><![CDATA[<p>In this Astro tutorial, we'll build an interactive coffee blog using Astro's core concepts, including file-based routing, build-time and live content collections, and the islands architecture.</p>
<p><a href="https://astro.build/" target="_blank" rel="noopener noreferrer">Astro</a> is a performance-friendly meta-framework for content-heavy websites. It ships zero JavaScript by default, but you can add it wherever you need interactivity, which is one reason Astro sites tend to have significantly better <a href="https://www.debugbear.com/docs/metrics/core-web-vitals">Core Web Vitals</a> than sites built with Next.js, Nuxt.js, WordPress, and other popular frameworks.</p>
<p>For example, <a href="https://httparchive.org/reports/techreport/tech?client=mobile&amp;tech=Astro,WordPress,Gatsby,Next.js,Nuxt.js&amp;geo=ALL&amp;rank=ALL&amp;page=1#comparison-good-cwvs" target="_blank" rel="noopener noreferrer">according to the HTTP Archive</a>, 71% of the tested Astro sites have a Good score on mobile at the time of writing <em>(on desktop, <a href="https://httparchive.org/reports/techreport/tech?client=desktop&amp;tech=Astro,WordPress,Gatsby,Next.js,Nuxt.js&amp;geo=ALL&amp;rank=ALL&amp;page=1#comparison-good-cwvs" target="_blank" rel="noopener noreferrer">it's 84%</a>)</em>:</p>
<p><img loading="lazy" alt="Good Core Web Vitals scores on mobile in August 2026, chart, results - Astro 71 percent, WordPress 48 percent, Gatsby 45 percent, Next.js 35 percent, Nuxt.js 28 percent" src="https://www.debugbear.com/assets/images/astro-core-web-vitals-http-archive-0f221b77cf11e88534b513976273289a.png" width="1000" height="722" data-abc="true" class="img_CujE"></p>
<p>Check out the <a href="https://www.debugbear.com/dimg/astro-tutorial.pdf" target="_blank" rel="noopener noreferrer">PDF version of this tutorial</a>.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">What We Will Build<a href="https://www.debugbear.com/blog/astro-tutorial#what-we-will-build" class="hash-link" aria-label="Direct link to What We Will Build" title="Direct link to What We Will Build">​</a></h2>
<p>Our demo site will feature:</p>
<ul>
<li>A homepage with:<!-- -->
<ul>
<li>a random coffee recommendation, powered by a live content collection that fetches data from an external API</li>
<li>the latest blog posts, powered by a build-time content collection</li>
</ul>
</li>
<li>Three blog posts, each with:<!-- -->
<ul>
<li>a Like button, built as a <a href="https://preactjs.com/" target="_blank" rel="noopener noreferrer">Preact</a> island</li>
<li>a reading progress bar, built as a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components" target="_blank" rel="noopener noreferrer">Web Component</a></li>
</ul>
</li>
</ul>
<p>We'll deploy the site to <a href="https://www.cloudflare.com/en-gb/developer-platform/products/workers/" target="_blank" rel="noopener noreferrer">Cloudflare Workers</a>.</p>
<p>The goal of this setup is to demonstrate that Astro <a href="https://docs.astro.build/en/guides/framework-components/" target="_blank" rel="noopener noreferrer">supports several frontend frameworks</a> <em>(e.g., Preact, Svelte, Solid, Vue, React, etc.)</em> alongside Web Components.</p>
<p>Technically, this means you could install several UI framework integrations side by side, with each one <a href="https://www.debugbear.com/blog/javascript-hydration">hydrating</a> as a separate Astro island. On the other hand, Astro doesn't add Web Components as islands, since they just use built-in browser APIs. This lets us compare adding interactivity to an Astro site with and without using the islands architecture.</p>
<p>To keep the coffee blog fast, we'll use Preact instead of React because it provides a React-like API in about 3KB <a href="https://www.debugbear.com/blog/http-compression-gzip-brotli">gzipped</a>, compared to React's 40KB+. If you already <a href="https://www.debugbear.com/blog/measuring-react-app-performance">know React</a>, most of what you know transfers directly to Preact <em>(e.g., components, JSX, hooks)</em>, aside from <a href="https://preactjs.com/guide/v10/differences-to-react/" target="_blank" rel="noopener noreferrer">a few small differences</a>.</p>
<div class="theme-admonition theme-admonition-tip admonition_LMjb alert alert--success"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>tip</div><div class="admonitionContent_pGk6"><p>You can find:</p><ul>
<li><a href="https://github.com/azaleamollis/coffee-blog-astro" target="_blank" rel="noopener noreferrer">the source code on GitHub</a></li>
<li><a href="https://astro.perftuts.workers.dev/" target="_blank" rel="noopener noreferrer">the final result on Cloudflare Workers</a></li>
</ul></div></div>
<p>Here's a screenshot of the homepage with the three blog posts and the random coffee recommendation at the top:</p>
<p><img loading="lazy" alt="Astro demo blog, homepage" src="https://www.debugbear.com/assets/images/astro-demo-blog-homepage-27a410a8c86f67e096df9d1679189512.png" width="1000" height="706" data-abc="true" class="img_CujE"></p>
<p>A single blog post shows the Like button and the reading progress bar at the top:</p>
<p><img loading="lazy" alt="Astro demo blog, a single post" src="https://www.debugbear.com/assets/images/astro-demo-blog-single-post-2680bc11daaf07d9761fe5294c8d5232.png" width="1000" height="663" data-abc="true" class="img_CujE"></p>
<p>The finished <a href="https://docs.astro.build/en/basics/project-structure/#directories-and-files" target="_blank" rel="noopener noreferrer">project structure</a> looks like this:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-txt codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-txt codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">coffee-blog/</span><br></span><span class="token-line"><span class="token plain">├── public/                                 ← static assets, served as-is</span><br></span><span class="token-line"><span class="token plain">│   └── favicon.svg</span><br></span><span class="token-line"><span class="token plain">├── src/</span><br></span><span class="token-line"><span class="token plain">│   ├── components/</span><br></span><span class="token-line"><span class="token plain">│   │   ├── CoffeeRecommendation.astro      ← uses a live content collection</span><br></span><span class="token-line"><span class="token plain">│   │   ├── LikeButton.css                  ← unscoped CSS for the Preact island</span><br></span><span class="token-line"><span class="token plain">│   │   ├── LikeButton.jsx                  ← Preact island</span><br></span><span class="token-line"><span class="token plain">│   │   └── ReadingProgressBar.astro        ← renders a Web Component</span><br></span><span class="token-line"><span class="token plain">│   ├── content/</span><br></span><span class="token-line"><span class="token plain">│   │   └── blog/                           ← uses a build-time content collection</span><br></span><span class="token-line"><span class="token plain">│   │       ├── grind-size-matters.md</span><br></span><span class="token-line"><span class="token plain">│   │       ├── our-v60-ratio.md</span><br></span><span class="token-line"><span class="token plain">│   │       └── pour-over-vs-french-press.md</span><br></span><span class="token-line"><span class="token plain">│   ├── layouts/                            ← shared page layouts</span><br></span><span class="token-line"><span class="token plain">│   │   └── Layout.astro</span><br></span><span class="token-line"><span class="token plain">│   ├── live/                               ← live loader logic</span><br></span><span class="token-line"><span class="token plain">│   │   └── coffee-loader.ts</span><br></span><span class="token-line"><span class="token plain">│   ├── pages/</span><br></span><span class="token-line"><span class="token plain">│   │   ├── blog/</span><br></span><span class="token-line"><span class="token plain">│   │   │   └── [slug].astro                ← blog post page (file-based routing)</span><br></span><span class="token-line"><span class="token plain">│   │   └── index.astro                     ← homepage</span><br></span><span class="token-line"><span class="token plain">│   ├── styles/                             ← shared design tokens</span><br></span><span class="token-line"><span class="token plain">│   │   └── global.css</span><br></span><span class="token-line"><span class="token plain">│   ├── content.config.ts                   ← build-time content collection config</span><br></span><span class="token-line"><span class="token plain">│   └── live.config.ts                      ← live content collection config</span><br></span><span class="token-line"><span class="token plain">├── astro.config.mjs                        ← adapter + integrations</span><br></span><span class="token-line"><span class="token plain">├── package.json                            ← dependencies</span><br></span><span class="token-line"><span class="token plain">├── tsconfig.json                           ← TypeScript config</span><br></span><span class="token-line"><span class="token plain">└── wrangler.jsonc                          ← Cloudflare Worker config</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<h2 class="anchor anchorWithStickyNavbar_FNw8">Prerequisites<a href="https://www.debugbear.com/blog/astro-tutorial#prerequisites" class="hash-link" aria-label="Direct link to Prerequisites" title="Direct link to Prerequisites">​</a></h2>
<p>If you've written any HTML, CSS, and JavaScript before, you already have most of the knowledge you'll need to build an Astro site.</p>
<p>A few files in this tutorial use basic <a href="https://www.typescriptlang.org/" target="_blank" rel="noopener noreferrer">TypeScript</a> to catch errors at build time. Don't worry if you haven't used it before, since TypeScript is just JavaScript with type annotations added on top. While Astro works with plain JavaScript too, it <a href="https://docs.astro.build/en/guides/typescript/" target="_blank" rel="noopener noreferrer">supports TypeScript</a> out of the box, so there's nothing extra to install.</p>
<p>Beyond that, you'll also need:</p>
<ul>
<li>Node.js 22.12+</li>
<li>A JavaScript package manager, such as npm, pnpm, or Yarn</li>
<li>A code editor, such as <a href="https://vscodium.com/" target="_blank" rel="noopener noreferrer">VSCodium</a></li>
<li>A GitHub or GitLab account to host the source code</li>
<li>A Cloudflare account</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_FNw8">1. Scaffold the Project<a href="https://www.debugbear.com/blog/astro-tutorial#1-scaffold-the-project" class="hash-link" aria-label="Direct link to 1. Scaffold the Project" title="Direct link to 1. Scaffold the Project">​</a></h2>
<p>To get started with Astro development, run the following command in your terminal:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-bash codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-bash codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">npm create astro@latest</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>When prompted, pick the <strong>Use minimal (empty) template</strong> option, then go with the default options for the rest of the questions:</p>
<p><img loading="lazy" alt="Astro installer, template prompt with four options in the terminal - 1. A basic, helpful starter project, 2. Use blog template, 3. Use docs (Starlight) template, 4. Use minimal (empty) template - the fourth option highlighted" src="https://www.debugbear.com/assets/images/astro-installation-prompt-template-d53a1257e4a387180bb369662d579261.png" width="747" height="325" data-abc="true" class="img_CujE"></p>
<p>Once the installer finishes, you can start the dev server by running <code>npm run dev</code> and opening the site at <code>localhost:4321</code> in your browser. Right now, it's just a bare-bones placeholder page; we'll be adding the other folders and files as we go.</p>
<p>For starters, let's replace Astro's default favicon files with an SVG containing the <a href="https://emojipedia.org/hot-beverage" target="_blank" rel="noopener noreferrer">hot beverage emoji</a>:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-svg codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-svg codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token comment">&lt;!-- /public/favicon.svg --&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">svg</span><span class="token tag"> </span><span class="token tag attr-name">xmlns</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">http://www.w3.org/2000/svg</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag attr-name">viewBox</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">0 0 100 100</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">text</span><span class="token tag"> </span><span class="token tag attr-name">y</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">.9em</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag attr-name">font-size</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">75</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain">☕</span><span class="token tag punctuation">&lt;/</span><span class="token tag">text</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">svg</span><span class="token tag punctuation">&gt;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>Place the <code>favicon.svg</code> file into the <a href="https://docs.astro.build/en/basics/project-structure/#public" target="_blank" rel="noopener noreferrer"><code>public</code></a> folder, which is the home for non-code, static assets that Astro serves as-is without processing them, such as fonts, icons, or the <code>robots.txt</code> file.</p>
<div class="theme-admonition theme-admonition-tip admonition_LMjb alert alert--success"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>tip</div><div class="admonitionContent_pGk6"><p>If you still see the default Astro favicon in the browser tab, clear the cache, as some browsers are prone to <a href="https://opengraph-check.com/en/blog/favicon-cache-explained" target="_blank" rel="noopener noreferrer">aggressive favicon caching</a>.</p></div></div>
<div class="theme-admonition theme-admonition-warning admonition_LMjb alert alert--warning"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>warning</div><div class="admonitionContent_pGk6"><p>While using an emoji glyph as a favicon is fine for a hobby project, a production site should use an actual SVG path instead.</p></div></div>
<h2 class="anchor anchorWithStickyNavbar_FNw8">2. Set Up the Base Layout and Global Styles<a href="https://www.debugbear.com/blog/astro-tutorial#2-set-up-the-base-layout-and-global-styles" class="hash-link" aria-label="Direct link to 2. Set Up the Base Layout and Global Styles" title="Direct link to 2. Set Up the Base Layout and Global Styles">​</a></h2>
<p>First, we define the layout every page will share and the global styles for the site. Create the following folders and empty files in the <code>src</code> folder:</p>
<ul>
<li><code>/layouts/Layout.astro</code></li>
<li><code>/pages/index.astro</code></li>
<li><code>/styles/global.css</code></li>
</ul>
<p>An <code>.astro</code> file is <a href="https://docs.astro.build/en/reference/astro-syntax/" target="_blank" rel="noopener noreferrer">Astro's own component format</a>. It looks similar to <a href="https://en.wikipedia.org/wiki/JavaScript_XML" target="_blank" rel="noopener noreferrer">JSX (JavaScript XML)</a>, but the Astro syntax is a superset of HTML with support for components and JavaScript expressions. It has two main parts:</p>
<ol>
<li>The <strong>component script</strong>, also known as the <strong>frontmatter</strong>, where you write server-side JavaScript or TypeScript <em>(you place this at the top of the file between two <code>---</code> code fences)</em>.</li>
<li>The <strong>template</strong>, where you define the HTML that Astro renders. You can use the <a href="https://docs.astro.build/en/reference/astro-syntax/#variables" target="_blank" rel="noopener noreferrer"><code>{}</code></a> notation to embed values or expressions from the component script into the markup.</li>
</ol>
<p><code>Layout.astro</code> is a shared wrapper every page will use. It defines the basic HTML structure of the site:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-html codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-html codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain">// /src/layouts/Layout.astro</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">import '../styles/global.css';</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">// Defines the props this layout expects</span><br></span><span class="token-line"><span class="token plain">interface Props {</span><br></span><span class="token-line"><span class="token plain">  title: string;</span><br></span><span class="token-line"><span class="token plain">}</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">const { title } = Astro.props;</span><br></span><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token doctype punctuation">&lt;!</span><span class="token doctype doctype-tag">doctype</span><span class="token doctype"> </span><span class="token doctype name">html</span><span class="token doctype punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">html</span><span class="token tag"> </span><span class="token tag attr-name">lang</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">en</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">head</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">meta</span><span class="token tag"> </span><span class="token tag attr-name">charset</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">UTF-8</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">meta</span><span class="token tag"> </span><span class="token tag attr-name">name</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">viewport</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag attr-name">content</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">width=device-width</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">link</span><span class="token tag"> </span><span class="token tag attr-name">rel</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">icon</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag attr-name">type</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">image/svg+xml</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag attr-name">href</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">/favicon.svg</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">title</span><span class="token tag punctuation">&gt;</span><span class="token plain">{title}</span><span class="token tag punctuation">&lt;/</span><span class="token tag">title</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;/</span><span class="token tag">head</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">body</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">header</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token tag punctuation">&lt;</span><span class="token tag">a</span><span class="token tag"> </span><span class="token tag attr-name">href</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">/</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain">☕ Coffee Notes from Underground</span><span class="token tag punctuation">&lt;/</span><span class="token tag">a</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;/</span><span class="token tag">header</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">main</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token comment">&lt;!-- Renders the page content passed into this layout --&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token tag punctuation">&lt;</span><span class="token tag">slot</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;/</span><span class="token tag">main</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;/</span><span class="token tag">body</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">html</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">style</span><span class="token tag punctuation">&gt;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css selector">header</span><span class="token style language-css"> </span><span class="token style language-css punctuation">{</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">padding</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">2</span><span class="token style language-css unit">rem</span><span class="token style language-css"> </span><span class="token style language-css number">0</span><span class="token style language-css"> </span><span class="token style language-css number">1</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css punctuation">}</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css selector">header a</span><span class="token style language-css"> </span><span class="token style language-css punctuation">{</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">text-decoration</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> none</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-weight</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> bold</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-size</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">1.1</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">color</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css function">var</span><span class="token style language-css punctuation">(</span><span class="token style language-css variable">--coffee-dark</span><span class="token style language-css punctuation">)</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css punctuation">}</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">style</span><span class="token tag punctuation">&gt;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>The <a href="https://docs.astro.build/en/basics/astro-components/#slots" target="_blank" rel="noopener noreferrer"><code>&lt;slot /&gt;</code></a> element is Astro's own template syntax that defines where each page's content is rendered.</p>
<p>The file also includes styles for the header. We keep them here to <a href="https://koenvangilst.nl/lab/code-colocation-is-king" target="_blank" rel="noopener noreferrer">colocate</a> the styles with the markup they affect, rather than putting them in the global CSS file. Astro also scopes <code>&lt;style&gt;</code> blocks in <code>.astro</code> files to the component by default, so these styles apply only to this component and don't leak into others. Since the layout is the only file on the site that renders the header, keeping its styles here makes them easier to find and maintain.</p>
<p>To the <code>/src/pages/index.astro</code> file, we just add an <code>&lt;h1&gt;</code> heading for now to have something visible on the homepage:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-html codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-html codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain">// /src/pages/index.astro</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">import Layout from '../layouts/Layout.astro';</span><br></span><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">Layout</span><span class="token tag"> </span><span class="token tag attr-name">title</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">Coffee Notes from Underground</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">h1</span><span class="token tag punctuation">&gt;</span><span class="token plain">Brewing Tips from Our Baristas</span><span class="token tag punctuation">&lt;/</span><span class="token tag">h1</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">Layout</span><span class="token tag punctuation">&gt;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>We also add some basic CSS to <code>global.css</code> that includes design tokens and global styles that are shared across the entire site:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-css codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-css codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token comment">/* /src/styles/global.css */</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token selector pseudo-class">:root</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token variable">--coffee-dark</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token hexcode color">#3b2412</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token variable">--coffee-medium</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token hexcode color">#6f4e37</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token variable">--coffee-light</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token hexcode color">#b08968</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token variable">--cream</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token hexcode color">#f3e9dd</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token variable">--paper</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token hexcode color">#fffaf3</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token variable">--text</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token hexcode color">#2b1d13</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token variable">--text-muted</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token hexcode color">#7a6a5a</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token variable">--accent</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token hexcode color">#ec6e07</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token variable">--text-on-accent</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token hexcode color">#ffffff</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token selector">*</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">box-sizing</span><span class="token punctuation">:</span><span class="token plain"> border-box</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token selector">body</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">font-family</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token string">'Georgia'</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">'Iowan Old Style'</span><span class="token punctuation">,</span><span class="token plain"> serif</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">background</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--paper</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">color</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--text</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">max-width</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token number">680</span><span class="token unit">px</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">margin</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token number">0</span><span class="token plain"> auto</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">padding</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token number">0</span><span class="token plain"> </span><span class="token number">1.5</span><span class="token unit">rem</span><span class="token plain"> </span><span class="token number">4</span><span class="token unit">rem</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">line-height</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token number">1.7</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token selector">h1</span><span class="token selector punctuation">,</span><span class="token selector"></span><br></span><span class="token-line"><span class="token selector">h2</span><span class="token selector punctuation">,</span><span class="token selector"></span><br></span><span class="token-line"><span class="token selector">h3</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">font-family</span><span class="token punctuation">:</span><span class="token plain"> system-ui</span><span class="token punctuation">,</span><span class="token plain"> sans-serif</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">line-height</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token number">1.2</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token selector">a</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">color</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--coffee-medium</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token selector">code</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">background</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--cream</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">padding</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token number">0.1</span><span class="token unit">em</span><span class="token plain"> </span><span class="token number">0.4</span><span class="token unit">em</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">border-radius</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token number">4</span><span class="token unit">px</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">font-size</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token number">0.9</span><span class="token unit">em</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>If you now open the site in your browser, this is what you should see:</p>
<p><img loading="lazy" alt="Placeholder homepage on the dev server showing the Astro dev toolbar with the Audit modal open" src="https://www.debugbear.com/assets/images/build-the-homepage-astro-toolbar-8e3674830b91829ec2b64243b69182ed.png" width="1000" height="699" data-abc="true" class="img_CujE"></p>
<p>The screenshot above also shows <a href="https://docs.astro.build/en/guides/dev-toolbar/" target="_blank" rel="noopener noreferrer">Astro's dev toolbar</a> with the <strong>Audit</strong> menu open. The dev toolbar is enabled by default while running the site in development mode.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">3. Add the Blog Posts with a Build-Time Content Collection<a href="https://www.debugbear.com/blog/astro-tutorial#3-add-the-blog-posts-with-a-build-time-content-collection" class="hash-link" aria-label="Direct link to 3. Add the Blog Posts with a Build-Time Content Collection" title="Direct link to 3. Add the Blog Posts with a Build-Time Content Collection">​</a></h2>
<p>To add the three blog posts, create the following empty files in the <code>src</code> folder <em>(or use different slugs and content for the posts if you prefer)</em>:</p>
<ul>
<li><code>/content.config.ts</code></li>
<li><code>/content/blog/grind-size-matters.md</code></li>
<li><code>/content/blog/our-v60-ratio.md</code></li>
<li><code>/content/blog/pour-over-vs-french-press.md</code></li>
</ul>
<p>The <a href="https://docs.astro.build/en/guides/content-collections/#defining-build-time-content-collections" target="_blank" rel="noopener noreferrer"><code>content.config.ts</code></a> file defines where to find the posts and what data each post's frontmatter must contain:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-ts codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-ts codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token comment">/* /src/content.config.ts */</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> defineCollection </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'astro:content'</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> z </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'astro/zod'</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> glob </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'astro/loaders'</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token comment">// Defines the blog collection and its schema</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> blog </span><span class="token operator">=</span><span class="token plain"> </span><span class="token function">defineCollection</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token comment">// Loads every Markdown file in src/content/blog/ as an entry</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  loader</span><span class="token operator">:</span><span class="token plain"> </span><span class="token function">glob</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"> pattern</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">'**/*.md'</span><span class="token punctuation">,</span><span class="token plain"> base</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">'./src/content/blog'</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  schema</span><span class="token operator">:</span><span class="token plain"> z</span><span class="token punctuation">.</span><span class="token function">object</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    title</span><span class="token operator">:</span><span class="token plain"> z</span><span class="token punctuation">.</span><span class="token function">string</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    pubDate</span><span class="token operator">:</span><span class="token plain"> z</span><span class="token punctuation">.</span><span class="token function">date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    tags</span><span class="token operator">:</span><span class="token plain"> z</span><span class="token punctuation">.</span><span class="token function">array</span><span class="token punctuation">(</span><span class="token plain">z</span><span class="token punctuation">.</span><span class="token function">string</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token comment">// Catches a typo like "medum" instead of allowing invalid data</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    roast</span><span class="token operator">:</span><span class="token plain"> z</span><span class="token punctuation">.</span><span class="token function">enum</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string">'light'</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">'medium'</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">'dark'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">optional</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    excerpt</span><span class="token operator">:</span><span class="token plain"> z</span><span class="token punctuation">.</span><span class="token function">string</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">export</span><span class="token plain"> </span><span class="token keyword">const</span><span class="token plain"> collections </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> blog </span><span class="token punctuation">}</span><span class="token punctuation">;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>The <a href="https://docs.astro.build/en/reference/content-loader-reference/#glob-loader" target="_blank" rel="noopener noreferrer"><code>glob()</code></a> loader tells Astro to treat every Markdown file in the <code>/src/content/blog/</code> folder that matches the <code>**/*.md</code> <a href="https://docs.astro.build/en/reference/content-loader-reference/#pattern" target="_blank" rel="noopener noreferrer">pattern</a> as an entry in the <code>blog</code> collection.</p>
<p>The <a href="https://docs.astro.build/en/guides/content-collections/#defining-the-collection-schema" target="_blank" rel="noopener noreferrer">collection schema</a> describes the structure and rules that the data must follow. We define it in <a href="https://docs.astro.build/en/reference/modules/astro-content/#definecollection" target="_blank" rel="noopener noreferrer"><code>defineCollection()</code></a> once, and then every post is validated against it automatically.</p>
<div class="theme-admonition theme-admonition-info admonition_LMjb alert alert--info"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</div><div class="admonitionContent_pGk6"><p>Schemas are optional for <code>defineCollection()</code>, but strongly recommended. Without one, Astro won't validate your collection entries or generate types for their frontmatter data. However, if you provide a schema, it must be built with <a href="https://zod.dev/" target="_blank" rel="noopener noreferrer">Zod</a>, because <code>defineCollection()</code> expects a Zod schema.</p></div></div>
<p>The blog posts themselves are Markdown files with a frontmatter block at the top <em>(for the full content of the three posts, <a href="https://github.com/azaleamollis/coffee-blog-astro/tree/main/src/content/blog" target="_blank" rel="noopener noreferrer">see the GitHub repository</a>)</em>:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-md codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-md codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token comment">&lt;!-- /src/content/blog/grind-size-matters.md --&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token hr punctuation">---</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">title: "Why Grind Size Matters More Than You Think"</span><br></span><span class="token-line"><span class="token plain">pubDate: 2026-08-10</span><br></span><span class="token-line"><span class="token plain">tags: ["brewing", "technique"]</span><br></span><span class="token-line"><span class="token plain">roast: light</span><br></span><span class="token-line"><span class="token plain"></span><span class="token title important">excerpt: "The single adjustment that fixes more bad cups than anything else."</span><br></span><span class="token-line"><span class="token title important"></span><span class="token title important punctuation">---</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">If a cup tastes sour, it's usually under-extracted — grind finer...</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>Right now, the homepage still looks the same because it doesn't load any content from the blog collection yet. To give each post its own page, we need to add routing.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">4. Route the Blog Posts<a href="https://www.debugbear.com/blog/astro-tutorial#4-route-the-blog-posts" class="hash-link" aria-label="Direct link to 4. Route the Blog Posts" title="Direct link to 4. Route the Blog Posts">​</a></h2>
<p>To create a page for each post in the <code>blog</code> content collection, we'll set up a dynamic route. Add the following empty file to the <code>src</code> folder:</p>
<ul>
<li><code>/pages/blog/[slug].astro</code></li>
</ul>
<p>The square brackets around <code>[slug]</code> mark a <a href="https://docs.astro.build/en/guides/routing/#static-ssg-mode" target="_blank" rel="noopener noreferrer">dynamic route segment</a> <em>(one file that generates multiple pages at build time)</em>.</p>
<p>By default, Astro uses <a href="https://docs.astro.build/en/basics/astro-pages/#file-based-routing" target="_blank" rel="noopener noreferrer">file-based routing</a>, which maps URLs to the corresponding files in the <code>/src/pages/</code> folder in the following way:</p>
<ul>
<li>Regular files automatically become pages <em>(e.g., <code>/src/pages/index.astro</code> becomes <code>/</code>)</em>.</li>
<li>Dynamic route files, such as <code>[slug].astro</code>, tell Astro what shape the URL will take through their filename and placement. For example, placing <code>[slug].astro</code> in the <code>/pages/blog/</code> folder defines a route for the blog posts, which will produce URLs such as <code>/blog/our-v60-ratio/</code> or <code>/blog/grind-size-matters/</code>. However, <code>[slug].astro</code> is a template rather than a page, so Astro still needs to know through <a href="https://docs.astro.build/en/reference/routing-reference/#getstaticpaths" target="_blank" rel="noopener noreferrer"><code>getStaticPaths()</code></a> which actual pages to build from it.</li>
</ul>
<p>The <code>/src/pages/blog/[slug].astro</code> file uses <code>getStaticPaths()</code> to generate a page for every entry in the <code>blog</code> collection:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-html codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-html codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain">// /src/pages/blog/[slug].astro</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">import Layout from '../../layouts/Layout.astro';</span><br></span><span class="token-line"><span class="token plain">import { getCollection, render } from 'astro:content';</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">// Generates one route for each blog post</span><br></span><span class="token-line"><span class="token plain">export async function getStaticPaths() {</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  // Gets all blog posts so we can generate a route for each one</span><br></span><span class="token-line"><span class="token plain">  const posts = await getCollection('blog');</span><br></span><span class="token-line"><span class="token plain">  </span><br></span><span class="token-line"><span class="token plain">  return posts.map((post) =&gt; ({</span><br></span><span class="token-line"><span class="token plain">    params: { slug: post.id },</span><br></span><span class="token-line"><span class="token plain">    props: { post },</span><br></span><span class="token-line"><span class="token plain">  }));</span><br></span><span class="token-line"><span class="token plain">}</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">const { post } = Astro.props;</span><br></span><span class="token-line"><span class="token plain">const { Content } = await render(post);</span><br></span><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">Layout</span><span class="token tag"> </span><span class="token tag attr-name">title</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value">{post.data.title}</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">a</span><span class="token tag"> </span><span class="token tag attr-name">href</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">/</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token entity named-entity">&amp;larr;</span><span class="token plain"> Back</span><span class="token tag punctuation">&lt;/</span><span class="token tag">a</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">h1</span><span class="token tag punctuation">&gt;</span><span class="token plain">{post.data.title}</span><span class="token tag punctuation">&lt;/</span><span class="token tag">h1</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">p</span><span class="token tag"> </span><span class="token tag attr-name">class</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">meta</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    {post.data.pubDate.toLocaleDateString('en-US', { dateStyle: 'long' })}</span><br></span><span class="token-line"><span class="token plain">    {post.data.roast &amp;&amp; ` · ${post.data.roast} roast`}</span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;/</span><span class="token tag">p</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">Content</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">Layout</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">style</span><span class="token tag punctuation">&gt;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css selector class">.meta</span><span class="token style language-css"> </span><span class="token style language-css punctuation">{</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">color</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css function">var</span><span class="token style language-css punctuation">(</span><span class="token style language-css variable">--text-muted</span><span class="token style language-css punctuation">)</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-family</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> system-ui</span><span class="token style language-css punctuation">,</span><span class="token style language-css"> sans-serif</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-size</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">0.9</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">margin-bottom</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">2</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css punctuation">}</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">style</span><span class="token tag punctuation">&gt;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<div class="theme-admonition theme-admonition-note admonition_LMjb alert alert--secondary"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_pGk6"><p>Notice <code>post.data.title</code>, <code>post.data.pubDate</code>, and <code>post.data.roast</code> above. We haven't written an interface for <code>post</code>. This is because Astro uses the Zod schema from <code>content.config.ts</code> to automatically determine the type of <code>post.data</code>. For example, <code>pubDate</code> is already a real <code>Date</code> object <em>(not, say, a <code>string</code>)</em>, which is why <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString" target="_blank" rel="noopener noreferrer"><code>.toLocaleDateString()</code></a> works on it.</p></div></div>
<p>With routing in place, you can now visit the individual blog posts. For example, visiting <code>http://localhost:4321/blog/our-v60-ratio/</code> should show:</p>
<p><img loading="lazy" alt="A screenshot of a static blog post, still with no interactive features" src="https://www.debugbear.com/assets/images/static-blog-post-no-interactivity-d43fa76f6f21de74d08cddb93fbeee21.png" width="1000" height="719" data-abc="true" class="img_CujE"></p>
<div class="theme-admonition theme-admonition-note admonition_LMjb alert alert--secondary"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_pGk6"><p>The black bar at the bottom of the screenshot above is Astro's collapsed dev toolbar, which you can expand by hovering over it.</p></div></div>
<p>Now that routing is set up, we can populate the <code>/pages/index.astro</code> file:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-html codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-html codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain">// /src/pages/index.astro</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">import Layout from '../layouts/Layout.astro';</span><br></span><span class="token-line"><span class="token plain">import { getCollection } from 'astro:content';</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">// Sorts posts by publish date, newest first</span><br></span><span class="token-line"><span class="token plain">const posts = (await getCollection('blog')).sort(</span><br></span><span class="token-line"><span class="token plain">  (a, b) =&gt; b.data.pubDate.valueOf() - a.data.pubDate.valueOf()</span><br></span><span class="token-line"><span class="token plain">);</span><br></span><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">Layout</span><span class="token tag"> </span><span class="token tag attr-name">title</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">Coffee Notes from Underground</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">h1</span><span class="token tag punctuation">&gt;</span><span class="token plain">Brewing Tips from Our Baristas</span><span class="token tag punctuation">&lt;/</span><span class="token tag">h1</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">ul</span><span class="token tag"> </span><span class="token tag attr-name">class</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">posts</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    {</span><br></span><span class="token-line"><span class="token plain">      posts.map((post) =&gt; (</span><br></span><span class="token-line"><span class="token plain">        </span><span class="token tag punctuation">&lt;</span><span class="token tag">li</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">          </span><span class="token tag punctuation">&lt;</span><span class="token tag">a</span><span class="token tag"> </span><span class="token tag attr-name">href</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value">{`/blog/${post.id}/`}</span><span class="token tag punctuation">&gt;</span><span class="token plain">{post.data.title}</span><span class="token tag punctuation">&lt;/</span><span class="token tag">a</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">          </span><span class="token tag punctuation">&lt;</span><span class="token tag">p</span><span class="token tag punctuation">&gt;</span><span class="token plain">{post.data.excerpt}</span><span class="token tag punctuation">&lt;/</span><span class="token tag">p</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">        </span><span class="token tag punctuation">&lt;/</span><span class="token tag">li</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      ))</span><br></span><span class="token-line"><span class="token plain">    }</span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;/</span><span class="token tag">ul</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">Layout</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">style</span><span class="token tag punctuation">&gt;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css selector class">.posts</span><span class="token style language-css"> </span><span class="token style language-css punctuation">{</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">list-style</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> none</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">padding</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">0</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">display</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> grid</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">gap</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">1.25</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css punctuation">}</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css selector class">.posts</span><span class="token style language-css selector"> li</span><span class="token style language-css"> </span><span class="token style language-css punctuation">{</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">border</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">1</span><span class="token style language-css unit">px</span><span class="token style language-css"> solid </span><span class="token style language-css function">var</span><span class="token style language-css punctuation">(</span><span class="token style language-css variable">--cream</span><span class="token style language-css punctuation">)</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">border-radius</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">12</span><span class="token style language-css unit">px</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">padding</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">1.25</span><span class="token style language-css unit">rem</span><span class="token style language-css"> </span><span class="token style language-css number">1.5</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">background</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css color">white</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css punctuation">}</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css selector class">.posts</span><span class="token style language-css selector"> a</span><span class="token style language-css"> </span><span class="token style language-css punctuation">{</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-family</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> system-ui</span><span class="token style language-css punctuation">,</span><span class="token style language-css"> sans-serif</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-weight</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">600</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-size</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">1.1</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">text-decoration</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> none</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css punctuation">}</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css selector class">.posts</span><span class="token style language-css selector"> p</span><span class="token style language-css"> </span><span class="token style language-css punctuation">{</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">margin</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">0.4</span><span class="token style language-css unit">rem</span><span class="token style language-css"> </span><span class="token style language-css number">0</span><span class="token style language-css"> </span><span class="token style language-css number">0</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">color</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css function">var</span><span class="token style language-css punctuation">(</span><span class="token style language-css variable">--text-muted</span><span class="token style language-css punctuation">)</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-size</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">1</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css punctuation">}</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">style</span><span class="token tag punctuation">&gt;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>The three blog posts should now appear on the homepage, ordered by date:</p>
<p><img loading="lazy" alt="A screenshot of the populated homepage, still with no interactive features" src="https://www.debugbear.com/assets/images/static-homepage-no-interactivity-abfe133fe1680723ee36bd9b03b91100.png" width="1000" height="569" data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">5. Add a Random Coffee Recommendation with a Live Content Collection<a href="https://www.debugbear.com/blog/astro-tutorial#5-add-a-random-coffee-recommendation-with-a-live-content-collection" class="hash-link" aria-label="Direct link to 5. Add a Random Coffee Recommendation with a Live Content Collection" title="Direct link to 5. Add a Random Coffee Recommendation with a Live Content Collection">​</a></h2>
<p>Our <a href="https://docs.astro.build/en/guides/content-collections/#live-content-collections" target="_blank" rel="noopener noreferrer">live content collection</a> will show a random recommendation fetched from <a href="https://sampleapis.com/api-list/coffee" target="_blank" rel="noopener noreferrer">Sample APIs' Coffee API</a> at request time. Create the following empty files in the <code>src</code> folder:</p>
<ol>
<li><code>/live/coffee-loader.ts</code></li>
<li><code>/live.config.ts</code></li>
<li><code>/components/CoffeeRecommendation.astro</code></li>
</ol>
<p>In the <code>coffee-loader.ts</code> file, we define a simple <a href="https://docs.astro.build/en/reference/content-loader-reference/#live-loaders" target="_blank" rel="noopener noreferrer">live loader</a> that fetches data from the API, filters the API results, randomly selects a recommendation, and falls back to a default recommendation if the request fails or returns no usable entries:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-ts codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-ts codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token comment">/* /src/live/coffee-loader.ts */</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token keyword">type</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> LiveLoader </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'astro/loaders'</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">interface</span><span class="token plain"> </span><span class="token class-name">Coffee</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  id</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  title</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  description</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> fallbackCoffee</span><span class="token operator">:</span><span class="token plain"> Coffee </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  id</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">0</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  title</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">'Black Coffee'</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  description</span><span class="token operator">:</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token string">'Black coffee is as simple as it gets with ground coffee beans steeped in hot water, served warm.'</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">export</span><span class="token plain"> </span><span class="token keyword">const</span><span class="token plain"> coffeeLoader</span><span class="token operator">:</span><span class="token plain"> LiveLoader</span><span class="token operator">&lt;</span><span class="token plain">Coffee</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> id</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  name</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">'coffee'</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token comment">// This loader only supports the single 'recommendation' entry.</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token comment">// loadCollection() is still required by the LiveLoader interface.</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token function-variable function">loadCollection</span><span class="token operator">:</span><span class="token plain"> </span><span class="token keyword">async</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token operator">=&gt;</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    entries</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token function-variable function">loadEntry</span><span class="token operator">:</span><span class="token plain"> </span><span class="token keyword">async</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"> filter </span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token operator">=&gt;</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token keyword">if</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token plain">filter</span><span class="token operator">?.</span><span class="token plain">id </span><span class="token operator">!==</span><span class="token plain"> </span><span class="token string">'recommendation'</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token keyword">return</span><span class="token plain"> </span><span class="token keyword">undefined</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token keyword">try</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token keyword">const</span><span class="token plain"> response </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword">await</span><span class="token plain"> </span><span class="token function">fetch</span><span class="token punctuation">(</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">        </span><span class="token string">'https://api.sampleapis.com/coffee/hot'</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token keyword">if</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token operator">!</span><span class="token plain">response</span><span class="token punctuation">.</span><span class="token plain">ok</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">        </span><span class="token keyword">throw</span><span class="token plain"> </span><span class="token keyword">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">Coffee API returned </span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">response</span><span class="token template-string interpolation punctuation">.</span><span class="token template-string interpolation">status</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token punctuation">}</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token keyword">const</span><span class="token plain"> coffees</span><span class="token operator">:</span><span class="token plain"> Coffee</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword">await</span><span class="token plain"> response</span><span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token comment">// Excludes test records that were added with higher IDs</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token keyword">const</span><span class="token plain"> recommendations </span><span class="token operator">=</span><span class="token plain"> coffees</span><span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">        </span><span class="token punctuation">(</span><span class="token plain">item</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token operator">=&gt;</span><span class="token plain"> item</span><span class="token punctuation">.</span><span class="token plain">id </span><span class="token operator">&gt;=</span><span class="token plain"> </span><span class="token number">1</span><span class="token plain"> </span><span class="token operator">&amp;&amp;</span><span class="token plain"> item</span><span class="token punctuation">.</span><span class="token plain">id </span><span class="token operator">&lt;=</span><span class="token plain"> </span><span class="token number">20</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token keyword">if</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token plain">recommendations</span><span class="token punctuation">.</span><span class="token plain">length </span><span class="token operator">===</span><span class="token plain"> </span><span class="token number">0</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">        </span><span class="token keyword">throw</span><span class="token plain"> </span><span class="token keyword">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">'Coffee API returned no recommendations'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token punctuation">}</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token keyword">const</span><span class="token plain"> coffee </span><span class="token operator">=</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">        recommendations</span><span class="token punctuation">[</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">          Math</span><span class="token punctuation">.</span><span class="token function">floor</span><span class="token punctuation">(</span><span class="token plain">Math</span><span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token operator">*</span><span class="token plain"> recommendations</span><span class="token punctuation">.</span><span class="token plain">length</span><span class="token punctuation">)</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">        </span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token keyword">return</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">        id</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">'recommendation'</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">        data</span><span class="token operator">:</span><span class="token plain"> coffee</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token punctuation">}</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">catch</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token plain">error</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Coffee API request failed:'</span><span class="token punctuation">,</span><span class="token plain"> error</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token keyword">return</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">        id</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">'recommendation'</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">        data</span><span class="token operator">:</span><span class="token plain"> fallbackCoffee</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token punctuation">}</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token punctuation">}</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token punctuation">;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>The <a href="https://docs.astro.build/en/reference/content-loader-reference/#defining-your-live-collection-with-your-loader" target="_blank" rel="noopener noreferrer"><code>live.config.ts</code></a> file registers the <code>coffeeLoader</code> object as the live collection's loader and <a href="https://docs.astro.build/en/guides/content-collections/#using-zod-schemas-with-live-collections" target="_blank" rel="noopener noreferrer">defines the shape of its entries with a Zod schema</a>:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-ts codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-ts codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token comment">/* /src/live.config.ts */</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> defineLiveCollection </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'astro:content'</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> z </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'astro/zod'</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> coffeeLoader </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'./live/coffee-loader'</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> coffee </span><span class="token operator">=</span><span class="token plain"> </span><span class="token function">defineLiveCollection</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  loader</span><span class="token operator">:</span><span class="token plain"> coffeeLoader</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  schema</span><span class="token operator">:</span><span class="token plain"> z</span><span class="token punctuation">.</span><span class="token function">object</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    title</span><span class="token operator">:</span><span class="token plain"> z</span><span class="token punctuation">.</span><span class="token function">string</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    description</span><span class="token operator">:</span><span class="token plain"> z</span><span class="token punctuation">.</span><span class="token function">string</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword">export</span><span class="token plain"> </span><span class="token keyword">const</span><span class="token plain"> collections </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> coffee </span><span class="token punctuation">}</span><span class="token punctuation">;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>In the <code>CoffeeRecommendation.astro</code> file, we query the live collection with <a href="https://docs.astro.build/en/reference/modules/astro-content/#getliveentry" target="_blank" rel="noopener noreferrer"><code>getLiveEntry()</code></a>, log any errors, show an unavailability notice if no entry is returned, and render the result on the homepage:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-html codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-html codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain">// /src/components/CoffeeRecommendation.astro</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">import { getLiveEntry } from 'astro:content';</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">const { entry, error } = await getLiveEntry('coffee', 'recommendation');</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">if (error) {</span><br></span><span class="token-line"><span class="token plain">  console.error('Could not load coffee recommendation:', error);</span><br></span><span class="token-line"><span class="token plain">}</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">section</span><span class="token tag"> </span><span class="token tag attr-name">class</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">coffee-recommend</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  {entry ? (</span><br></span><span class="token-line"><span class="token plain">    &lt;&gt;</span><br></span><span class="token-line"><span class="token plain">      </span><span class="token tag punctuation">&lt;</span><span class="token tag">h2</span><span class="token tag"> </span><span class="token tag attr-name">class</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">coffee-recommend-title</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">        We Recommend: {entry.data.title}</span><br></span><span class="token-line"><span class="token plain">      </span><span class="token tag punctuation">&lt;/</span><span class="token tag">h2</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      </span><span class="token tag punctuation">&lt;</span><span class="token tag">p</span><span class="token tag"> </span><span class="token tag attr-name">class</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">coffee-recommend-desc</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain">{entry.data.description}</span><span class="token tag punctuation">&lt;/</span><span class="token tag">p</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    &lt;/&gt;</span><br></span><span class="token-line"><span class="token plain">  ) : (</span><br></span><span class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">p</span><span class="token tag"> </span><span class="token tag attr-name">class</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">coffee-recommend-desc</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">      Today's recommendation is temporarily unavailable.</span><br></span><span class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;/</span><span class="token tag">p</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  )}</span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">section</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">style</span><span class="token tag punctuation">&gt;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css selector class">.coffee-recommend</span><span class="token style language-css"> </span><span class="token style language-css punctuation">{</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">display</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> block</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">padding</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">1</span><span class="token style language-css unit">rem</span><span class="token style language-css"> </span><span class="token style language-css number">1.5</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">border-radius</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">12</span><span class="token style language-css unit">px</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">background</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css function">var</span><span class="token style language-css punctuation">(</span><span class="token style language-css variable">--cream</span><span class="token style language-css punctuation">)</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">color</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css function">var</span><span class="token style language-css punctuation">(</span><span class="token style language-css variable">--coffee-dark</span><span class="token style language-css punctuation">)</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-family</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> system-ui</span><span class="token style language-css punctuation">,</span><span class="token style language-css"> sans-serif</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-size</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">0.9</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">margin</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">0</span><span class="token style language-css"> </span><span class="token style language-css number">0</span><span class="token style language-css"> </span><span class="token style language-css number">2</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css punctuation">}</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css selector class">.coffee-recommend-title</span><span class="token style language-css"> </span><span class="token style language-css punctuation">{</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-size</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">1.1</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-weight</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">600</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css punctuation">}</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css selector class">.coffee-recommend-desc</span><span class="token style language-css"> </span><span class="token style language-css punctuation">{</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-size</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">0.9375</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">font-style</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> italic</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">margin-top</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">0.25</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css punctuation">}</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">style</span><span class="token tag punctuation">&gt;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<div class="theme-admonition theme-admonition-note admonition_LMjb alert alert--secondary"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_pGk6"><p>The "temporarily unavailable" message above is shown when <code>getLiveEntry()</code> returns no entry. API failures are handled separately by <code>coffee-loader.ts</code>, which catches them and returns <code>fallbackCoffee</code> as a normal entry, so visitors still see a recommendation.</p></div></div>
<p>Finally, we need to change the homepage so it displays the coffee recommendation.</p>
<p>We first (1) add <code>export const prerender = false</code> to opt the homepage into <a href="https://docs.astro.build/en/guides/on-demand-rendering/#enabling-on-demand-rendering" target="_blank" rel="noopener noreferrer">on-demand rendering</a>, allowing it to fetch the live collection data at request time. Then, we (2) import and (3) add the <code>CoffeeRecommendation</code> component:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-html codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-html codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain">// /src/pages/index.astro</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">// (1) Sets on-demand rendering because it reads from a live collection</span><br></span><span class="token-line"><span class="token plain">export const prerender = false;</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">import Layout from '../layouts/Layout.astro';</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">// (2) Imports the CoffeeRecommendation component</span><br></span><span class="token-line"><span class="token plain">import CoffeeRecommendation from '../components/CoffeeRecommendation.astro';</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">import { getCollection } from 'astro:content';</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">/* ... */</span><br></span><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">Layout</span><span class="token tag"> </span><span class="token tag attr-name">title</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">Coffee Notes from Underground</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">h1</span><span class="token tag punctuation">&gt;</span><span class="token plain">Brewing Tips from Our Baristas</span><span class="token tag punctuation">&lt;/</span><span class="token tag">h1</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token comment">&lt;!-- (3) Adds the CoffeeRecommendation component --&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">CoffeeRecommendation</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><br></span><span class="token-line"><span class="token plain"></span><span class="token comment">&lt;!-- (the rest of the file stays the same) --&gt;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<div class="theme-admonition theme-admonition-warning admonition_LMjb alert alert--warning"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>warning</div><div class="admonitionContent_pGk6"><p>While the code above works on the dev server, you'll need a <a href="https://docs.astro.build/en/guides/on-demand-rendering/#server-adapters" target="_blank" rel="noopener noreferrer">server adapter</a> to use on-demand rendering in production <em>(we'll see how to add the Cloudflare adapter in Step 7)</em>.</p></div></div>
<div class="theme-admonition theme-admonition-note admonition_LMjb alert alert--secondary"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>A Side Note about Server Islands</div><div class="admonitionContent_pGk6"><p>Instead of using on-demand rendering for the entire homepage, you can also keep the homepage prerendered and use a <a href="https://docs.astro.build/en/guides/server-islands/" target="_blank" rel="noopener noreferrer">server island</a> to defer just the coffee recommendation to a separate server request. To do so, remove the <code>export const prerender = false;</code> line and add the <a href="https://docs.astro.build/en/reference/directives-reference/#server-directives" target="_blank" rel="noopener noreferrer"><code>server:defer</code></a> directive to the component:</p><pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-html codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-html codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token tag punctuation">&lt;</span><span class="token tag">CoffeeRecommendation</span><span class="token tag"> </span><span class="token tag attr-name namespace">server:</span><span class="token tag attr-name">defer</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre><p>Note that server islands also require an adapter because the deferred component must be rendered on the server at runtime, when the page is requested.</p></div></div>
<p>Now, the coffee recommendation has been added to the homepage. To test it, refresh the page a few times and watch for the recommendation to change:</p>
<p><img loading="lazy" alt="Coffee recommendation added to the homepage using a live content collection" src="https://www.debugbear.com/assets/images/live-content-collection-coffee-recommendation-56d50a0d56f877c6b9892c5fe1a01376.png" width="1000" height="713" data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">6. Add Interactivity with and without Astro Islands<a href="https://www.debugbear.com/blog/astro-tutorial#6-add-interactivity-with-and-without-astro-islands" class="hash-link" aria-label="Direct link to 6. Add Interactivity with and without Astro Islands" title="Direct link to 6. Add Interactivity with and without Astro Islands">​</a></h2>
<p>We'll add interactivity to our coffee blog in two different ways:</p>
<ul>
<li>a Like button as a framework component <em>(Preact)</em></li>
<li>a reading progress bar as a Web Component</li>
</ul>
<p>Create the following empty files in the <code>src</code> folder:</p>
<ol>
<li><code>/src/components/LikeButton.jsx</code></li>
<li><code>/src/components/LikeButton.css</code></li>
<li><code>/src/components/ReadingProgressBar.astro</code></li>
</ol>
<p>Astro uses an <a href="https://docs.astro.build/en/concepts/islands/" target="_blank" rel="noopener noreferrer">islands architecture</a> for framework components, where interactive components can become <a href="https://docs.astro.build/en/concepts/islands/#client-islands" target="_blank" rel="noopener noreferrer">client islands</a> that Astro hydrates in the browser.</p>
<p>However, you can also build interactive components as <a href="https://docs.astro.build/en/guides/client-side-scripts/#web-components-with-custom-elements" target="_blank" rel="noopener noreferrer">Web Components</a> using standard browser APIs on any Astro site, without shipping a framework runtime to the browser.</p>
<p>Before looking at the code, here's a brief comparison of island-based and browser-native interactivity in Astro development:</p>
<table><thead><tr><th></th><th><strong>Island-based interactivity</strong></th><th><strong>Browser-native interactivity</strong></th></tr></thead><tbody><tr><td><strong>Astro island?</strong></td><td>Yes</td><td>No</td></tr><tr><td><strong>Example</strong></td><td><code>LikeButton.jsx</code></td><td><code>ReadingProgressBar.astro</code></td></tr><tr><td><strong>UI technology</strong></td><td>Preact, Svelte, Vue, React, etc.</td><td>Web Components / browser APIs</td></tr><tr><td><strong>Astro hydration</strong></td><td>Yes</td><td>No</td></tr><tr><td><strong>JavaScript</strong></td><td>Framework runtime + component code</td><td>Component code only</td></tr><tr><td><strong>Styling</strong></td><td>Unscoped CSS</td><td><code>&lt;style&gt;</code> in <code>.astro</code> file (<a href="https://docs.astro.build/en/guides/styling/#scoped-styles" target="_blank" rel="noopener noreferrer">Astro-scoped</a>)</td></tr><tr><td><strong>How it becomes interactive</strong></td><td><a href="https://docs.astro.build/en/reference/directives-reference/#client-directives" target="_blank" rel="noopener noreferrer"><code>client:*</code> directive</a></td><td><code>&lt;script&gt;</code> registers the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements" target="_blank" rel="noopener noreferrer">custom element</a></td></tr></tbody></table>
<h3 class="anchor anchorWithStickyNavbar_FNw8">i. Add a Like Button as a Preact Island<a href="https://www.debugbear.com/blog/astro-tutorial#i-add-a-like-button-as-a-preact-island" class="hash-link" aria-label="Direct link to i. Add a Like Button as a Preact Island" title="Direct link to i. Add a Like Button as a Preact Island">​</a></h3>
<p>To add the Preact Like button, run the following command to install the <a href="https://docs.astro.build/en/guides/integrations-guide/preact/" target="_blank" rel="noopener noreferrer">Preact integration</a> for Astro:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-bash codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-bash codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">npx astro add preact</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>The installer will make changes to your <code>astro.config.mjs</code> and <code>tsconfig.json</code> files. When asked, accept the changes.</p>
<p>The <code>LikeButton.jsx</code> file defines a Preact component with local state that updates the button's text and styling when clicked:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-jsx codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-jsx codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token comment">/* /src/components/LikeButton.jsx */</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword module">import</span><span class="token plain"> </span><span class="token imports punctuation">{</span><span class="token imports"> useState </span><span class="token imports punctuation">}</span><span class="token plain"> </span><span class="token keyword module">from</span><span class="token plain"> </span><span class="token string">'preact/hooks'</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword module">import</span><span class="token plain"> </span><span class="token string">'./LikeButton.css'</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token keyword module">export</span><span class="token plain"> </span><span class="token keyword module">default</span><span class="token plain"> </span><span class="token keyword">function</span><span class="token plain"> </span><span class="token function maybe-class-name">LikeButton</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><br></span><span class="token-line"><span class="token plain">  </span><span class="token comment">// The button starts in the "not liked" state</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token plain">liked</span><span class="token punctuation">,</span><span class="token plain"> setLiked</span><span class="token punctuation">]</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token function">useState</span><span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token keyword control-flow">return</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">button</span><span class="token tag"></span><br></span><span class="token-line"><span class="token tag">      </span><span class="token tag attr-name">class</span><span class="token tag script language-javascript script-punctuation punctuation">=</span><span class="token tag script language-javascript punctuation">{</span><span class="token tag script language-javascript template-string template-punctuation string">`</span><span class="token tag script language-javascript template-string string">like-button </span><span class="token tag script language-javascript template-string interpolation interpolation-punctuation punctuation">${</span><span class="token tag script language-javascript template-string interpolation">liked </span><span class="token tag script language-javascript template-string interpolation operator">?</span><span class="token tag script language-javascript template-string interpolation"> </span><span class="token tag script language-javascript template-string interpolation string">'is-liked'</span><span class="token tag script language-javascript template-string interpolation"> </span><span class="token tag script language-javascript template-string interpolation operator">:</span><span class="token tag script language-javascript template-string interpolation"> </span><span class="token tag script language-javascript template-string interpolation string">''</span><span class="token tag script language-javascript template-string interpolation interpolation-punctuation punctuation">}</span><span class="token tag script language-javascript template-string template-punctuation string">`</span><span class="token tag script language-javascript punctuation">}</span><span class="token tag"></span><br></span><span class="token-line"><span class="token tag">      </span><span class="token tag attr-name">aria-pressed</span><span class="token tag script language-javascript script-punctuation punctuation">=</span><span class="token tag script language-javascript punctuation">{</span><span class="token tag script language-javascript">liked</span><span class="token tag script language-javascript punctuation">}</span><span class="token tag"></span><br></span><span class="token-line"><span class="token tag">      </span><span class="token tag attr-name">onClick</span><span class="token tag script language-javascript script-punctuation punctuation">=</span><span class="token tag script language-javascript punctuation">{</span><span class="token tag script language-javascript punctuation">(</span><span class="token tag script language-javascript punctuation">)</span><span class="token tag script language-javascript"> </span><span class="token tag script language-javascript arrow operator">=&gt;</span><span class="token tag script language-javascript"> </span><span class="token tag script language-javascript function">setLiked</span><span class="token tag script language-javascript punctuation">(</span><span class="token tag script language-javascript punctuation">(</span><span class="token tag script language-javascript parameter">liked</span><span class="token tag script language-javascript punctuation">)</span><span class="token tag script language-javascript"> </span><span class="token tag script language-javascript arrow operator">=&gt;</span><span class="token tag script language-javascript"> </span><span class="token tag script language-javascript operator">!</span><span class="token tag script language-javascript">liked</span><span class="token tag script language-javascript punctuation">)</span><span class="token tag script language-javascript punctuation">}</span><span class="token tag"></span><br></span><span class="token-line"><span class="token tag">    </span><span class="token tag punctuation">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line"><span class="token plain-text">      </span><span class="token punctuation">{</span><span class="token plain">liked </span><span class="token operator">?</span><span class="token plain"> </span><span class="token string">'☕ Liked'</span><span class="token plain"> </span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">'🤎 Like this post'</span><span class="token punctuation">}</span><span class="token plain-text"></span><br></span><span class="token-line"><span class="token plain-text">    </span><span class="token tag punctuation">&lt;/</span><span class="token tag">button</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>Before adding the CSS, let's update the <code>/src/pages/blog/[slug].astro</code> file to see what the Like button looks like without styling:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-html codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-html codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain">// /src/pages/blog/[slug].astro</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">/* ... */</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">// (1) Imports the LikeButton component</span><br></span><span class="token-line"><span class="token plain">import LikeButton from '../../components/LikeButton.jsx';</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">/* ... */</span><br></span><span class="token-line"><span class="token plain">---</span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">Layout</span><span class="token tag"> </span><span class="token tag attr-name">title</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value">{post.data.title}</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token comment">&lt;!-- ... --&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token comment">&lt;!-- (2) Adds the Like button to the page --&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">LikeButton</span><span class="token tag"> </span><span class="token tag attr-name namespace">client:</span><span class="token tag attr-name">idle</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">Content</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">Layout</span><span class="token tag punctuation">&gt;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>The <a href="https://docs.astro.build/en/reference/directives-reference/#clientidle" target="_blank" rel="noopener noreferrer"><code>client:idle</code></a> directive tells Astro to hydrate the Preact component in the browser when the page has finished its initial work and the browser is idle. You can use different <code>client:*</code> directives to control when hydration happens. For example, <code>client:load</code> hydrates the component as soon as the page loads, while <code>client:visible</code> waits until the component enters the viewport.</p>
<div class="theme-admonition theme-admonition-note admonition_LMjb alert alert--secondary"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_pGk6"><p>The island architecture also applies when you use the same framework multiple times. For example, if your site had multiple Preact components, each one would be its own separate island, with its own hydration timing <em>(e.g., one could use <code>client:idle</code> while another uses <code>client:visible</code>)</em>. Astro, however, shares common framework code between those islands while keeping their component code separate.</p></div></div>
<p>Now open a blog post in the browser and test the button. If it works as intended, add the styling to the <code>/src/components/LikeButton.css</code> file:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-css codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-css codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token comment">/* /src/components/LikeButton.css */</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token selector class">.like-button</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">border</span><span class="token punctuation">:</span><span class="token plain"> none</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">border-radius</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token number">999</span><span class="token unit">px</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">padding</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token number">0.5</span><span class="token unit">rem</span><span class="token plain"> </span><span class="token number">1.2</span><span class="token unit">rem</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">font-family</span><span class="token punctuation">:</span><span class="token plain"> system-ui</span><span class="token punctuation">,</span><span class="token plain"> sans-serif</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">font-size</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token number">0.95</span><span class="token unit">rem</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">cursor</span><span class="token punctuation">:</span><span class="token plain"> pointer</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">background</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--cream</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">color</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--text</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">transition</span><span class="token punctuation">:</span><span class="token plain"> background </span><span class="token number">0.15</span><span class="token unit">s</span><span class="token plain"> ease</span><span class="token punctuation">,</span><span class="token plain"> color </span><span class="token number">0.15</span><span class="token unit">s</span><span class="token plain"> ease</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token selector class">.like-button</span><span class="token selector class">.is-liked</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">background</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--accent</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token property">color</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--text-on-accent</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token comment">/* Applies hover styles only on devices that support hovering </span><br></span><span class="token-line"><span class="token comment">   to avoid touch-device UX issues */</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token atrule rule">@media</span><span class="token atrule"> </span><span class="token atrule punctuation">(</span><span class="token atrule property">hover</span><span class="token atrule punctuation">:</span><span class="token atrule"> hover</span><span class="token atrule punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token selector class">.like-button</span><span class="token selector pseudo-class">:not</span><span class="token selector punctuation">(</span><span class="token selector class">.is-liked</span><span class="token selector punctuation">)</span><span class="token selector pseudo-class">:hover</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token property">background</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--accent</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token property">color</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--text-on-accent</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token selector class">.like-button</span><span class="token selector class">.is-liked</span><span class="token selector pseudo-class">:hover</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">    </span><span class="token property">background</span><span class="token punctuation">:</span><span class="token plain"> </span><span class="token function">var</span><span class="token punctuation">(</span><span class="token variable">--coffee-dark</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>The Like button is now done; this is what you should see in the browser:</p>
<p><img loading="lazy" alt="Like button, shown on the dev site" src="https://www.debugbear.com/assets/images/like-button-ddf0da18fa70aa0b9969da3a39ce5f25.png" width="1000" height="778" data-abc="true" class="img_CujE"></p>
<h3 class="anchor anchorWithStickyNavbar_FNw8">ii. Add a Reading Progress Bar as a Web Component<a href="https://www.debugbear.com/blog/astro-tutorial#ii-add-a-reading-progress-bar-as-a-web-component" class="hash-link" aria-label="Direct link to ii. Add a Reading Progress Bar as a Web Component" title="Direct link to ii. Add a Reading Progress Bar as a Web Component">​</a></h3>
<p>We create the reading progress bar as a Web Component. Because the component uses only browser APIs for its client-side behavior, we don't need:</p>
<ul>
<li>a framework runtime</li>
<li>a <code>client:*</code> directive</li>
<li>Astro frontmatter, since there is no server-side component code</li>
</ul>
<p>The <code>ReadingProgressBar.astro</code> file consists of a custom <code>&lt;reading-progress-bar&gt;</code> element and a <code>&lt;script&gt;</code> that defines its behavior:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-html codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-html codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token comment">&lt;!-- /src/components/ReadingProgressBar.astro --&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">reading-progress-bar</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">div</span><span class="token tag"> </span><span class="token tag attr-name">class</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">bar</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token tag punctuation">&lt;/</span><span class="token tag">div</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">reading-progress-bar</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">script</span><span class="token tag punctuation">&gt;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">  </span><span class="token script language-javascript keyword">class</span><span class="token script language-javascript"> </span><span class="token script language-javascript class-name">ReadingProgressBar</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword">extends</span><span class="token script language-javascript"> </span><span class="token script language-javascript class-name">HTMLElement</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">{</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript comment">// Stores the visible bar and the pending animation-frame ID</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript literal-property property">bar</span><span class="token script language-javascript operator">:</span><span class="token script language-javascript"> </span><span class="token script language-javascript maybe-class-name">HTMLElement</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">|</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword null nil">null</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword null nil">null</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript literal-property property">rafId</span><span class="token script language-javascript operator">:</span><span class="token script language-javascript"> number </span><span class="token script language-javascript operator">|</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword nil">undefined</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    scrollableHeight </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript number">0</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript comment">// Calculates how far the document can be scrolled vertically</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript function-variable function">updateScrollableHeight</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript arrow operator">=&gt;</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">{</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">scrollableHeight</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">        </span><span class="token script language-javascript dom variable">document</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">documentElement</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">scrollHeight</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">-</span><span class="token script language-javascript"> </span><span class="token script language-javascript dom variable">window</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">innerHeight</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript punctuation">}</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript comment">// Converts scroll position to a 0–1 value and clamps the result</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript function-variable function">updateProgress</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript arrow operator">=&gt;</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">{</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript keyword">const</span><span class="token script language-javascript"> scrollTop </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript dom variable">window</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">scrollY</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript keyword">const</span><span class="token script language-javascript"> progress </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">        </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">scrollableHeight</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">&gt;</span><span class="token script language-javascript"> </span><span class="token script language-javascript number">0</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">          </span><span class="token script language-javascript operator">?</span><span class="token script language-javascript"> </span><span class="token script language-javascript known-class-name class-name">Math</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript method function property-access">min</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript known-class-name class-name">Math</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript method function property-access">max</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript">scrollTop </span><span class="token script language-javascript operator">/</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">scrollableHeight</span><span class="token script language-javascript punctuation">,</span><span class="token script language-javascript"> </span><span class="token script language-javascript number">0</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">,</span><span class="token script language-javascript"> </span><span class="token script language-javascript number">1</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">          </span><span class="token script language-javascript operator">:</span><span class="token script language-javascript"> </span><span class="token script language-javascript number">0</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript keyword control-flow">if</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">bar</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">{</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">        </span><span class="token script language-javascript comment">// Sets the bar's width as a percentage of scroll progress</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">        </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">bar</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">style</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">width</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript template-string template-punctuation string">`</span><span class="token script language-javascript template-string interpolation interpolation-punctuation punctuation">${</span><span class="token script language-javascript template-string interpolation">progress </span><span class="token script language-javascript template-string interpolation operator">*</span><span class="token script language-javascript template-string interpolation"> </span><span class="token script language-javascript template-string interpolation number">100</span><span class="token script language-javascript template-string interpolation interpolation-punctuation punctuation">}</span><span class="token script language-javascript template-string string">%</span><span class="token script language-javascript template-string template-punctuation string">`</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript punctuation">}</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript comment">// Allows the next scroll or resize event to schedule another update</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">rafId</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword nil">undefined</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript punctuation">}</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript comment">// Schedules a progress update for the next animation frame</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript function-variable function">onScroll</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript arrow operator">=&gt;</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">{</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript keyword control-flow">if</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">rafId</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">===</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword nil">undefined</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">{</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">        </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">rafId</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript function">requestAnimationFrame</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">updateProgress</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript punctuation">}</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript punctuation">}</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript comment">// Schedules a scrollable-height recalculation and progress update</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript comment">// for the next animation frame</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript function-variable function">onResize</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript arrow operator">=&gt;</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">{</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript keyword control-flow">if</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">rafId</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">===</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword nil">undefined</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">{</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">        </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">rafId</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript function">requestAnimationFrame</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript arrow operator">=&gt;</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">{</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">          </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript method function property-access">updateScrollableHeight</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">          </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript method function property-access">updateProgress</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">        </span><span class="token script language-javascript punctuation">}</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript punctuation">}</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript punctuation">}</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript function">connectedCallback</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">{</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript comment">// Finds the visible bar that represents reading progress</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">bar</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">querySelector</span><span class="token script language-javascript operator">&lt;</span><span class="token script language-javascript maybe-class-name">HTMLElement</span><span class="token script language-javascript operator">&gt;</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript string">'.bar'</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript comment">// Waits until the next animation frame before measuring the document</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript function">requestAnimationFrame</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript arrow operator">=&gt;</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">{</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">        </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript method function property-access">updateScrollableHeight</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">        </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript method function property-access">updateProgress</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript punctuation">}</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript dom variable">window</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript method function property-access">addEventListener</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript string">'scroll'</span><span class="token script language-javascript punctuation">,</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">onScroll</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript dom variable">window</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript method function property-access">addEventListener</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript string">'resize'</span><span class="token script language-javascript punctuation">,</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">onResize</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript punctuation">}</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript function">disconnectedCallback</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">{</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript comment">// Removes listeners and cancels any pending update</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript dom variable">window</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript method function property-access">removeEventListener</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript string">'scroll'</span><span class="token script language-javascript punctuation">,</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">onScroll</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript dom variable">window</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript method function property-access">removeEventListener</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript string">'resize'</span><span class="token script language-javascript punctuation">,</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">onResize</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript keyword control-flow">if</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">rafId</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">!==</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword nil">undefined</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript function">cancelAnimationFrame</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">rafId</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">      </span><span class="token script language-javascript keyword">this</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript property-access">rafId</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword nil">undefined</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">    </span><span class="token script language-javascript punctuation">}</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">  </span><span class="token script language-javascript punctuation">}</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript">  customElements</span><span class="token script language-javascript punctuation">.</span><span class="token script language-javascript method function property-access">define</span><span class="token script language-javascript punctuation">(</span><span class="token script language-javascript string">'reading-progress-bar'</span><span class="token script language-javascript punctuation">,</span><span class="token script language-javascript"> </span><span class="token script language-javascript maybe-class-name">ReadingProgressBar</span><span class="token script language-javascript punctuation">)</span><span class="token script language-javascript punctuation">;</span><span class="token script language-javascript"></span><br></span><span class="token-line"><span class="token script language-javascript"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">script</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">style</span><span class="token tag punctuation">&gt;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css selector">reading-progress-bar</span><span class="token style language-css"> </span><span class="token style language-css punctuation">{</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">position</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> fixed</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">top</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">0</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">left</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">0</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">display</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> block</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">width</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">100</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">height</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">10</span><span class="token style language-css unit">px</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">z-index</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">50</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css punctuation">}</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css selector class">.bar</span><span class="token style language-css"> </span><span class="token style language-css punctuation">{</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">height</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">100</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">width</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css number">0</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">    </span><span class="token style language-css property">background</span><span class="token style language-css punctuation">:</span><span class="token style language-css"> </span><span class="token style language-css function">var</span><span class="token style language-css punctuation">(</span><span class="token style language-css variable">--accent</span><span class="token style language-css punctuation">)</span><span class="token style language-css punctuation">;</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css">  </span><span class="token style language-css punctuation">}</span><span class="token style language-css"></span><br></span><span class="token-line"><span class="token style language-css"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">style</span><span class="token tag punctuation">&gt;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<div class="theme-admonition theme-admonition-note admonition_LMjb alert alert--secondary"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_pGk6"><p>The <code>&lt;reading-progress-bar&gt;</code> component uses the light DOM rather than <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM" target="_blank" rel="noopener noreferrer">Shadow DOM</a>. This is because Astro already scopes the component's <code>&lt;style&gt;</code> block, providing the CSS isolation needed here without requiring Shadow DOM.</p></div></div>
<p>The <code>connectedCallback()</code> and <code>disconnectedCallback()</code> methods above are <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks" target="_blank" rel="noopener noreferrer">lifecycle callbacks for Web Components</a>:</p>
<ul>
<li><code>connectedCallback()</code> finds the bar element, measures the page's scrollable height, sets the initial progress, and adds the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event" target="_blank" rel="noopener noreferrer"><code>scroll</code></a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event" target="_blank" rel="noopener noreferrer"><code>resize</code></a> event listeners.</li>
<li><code>disconnectedCallback()</code> removes both listeners and cancels any pending update when the element is removed, so the component cleans up after itself.</li>
</ul>
<p>Both the <code>scroll</code> and <code>resize</code> listeners schedule their updates with <a href="https://www.debugbear.com/blog/requestanimationframe"><code>requestAnimationFrame()</code></a>, rather than running on every event. A shared <code>rafId</code> field makes sure only one update is ever pending at a time. If multiple scroll or resize events fire before that update runs, they're effectively batched into the same animation-frame update.</p>
<div class="theme-admonition theme-admonition-note admonition_LMjb alert alert--secondary"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_pGk6"><p>The <code>requestAnimationFrame()</code> method schedules the update to run right before the browser's next paint, so the bar's width is written at the same cadence the browser is already rendering at. It never runs more often than a new frame can actually be shown, and always in sync with it. That makes it a better fit here than a fixed timer such as <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout" target="_blank" rel="noopener noreferrer"><code>setTimeout()</code></a>, which throttles by a chosen interval but has no relationship to the browser's own paint timing. For more details, see our <a href="https://www.debugbear.com/blog/requestanimationframe">guide to requestAnimationFrame</a>.</p></div></div>
<p>Finally, we need to add the <code>&lt;ReadingProgressBar&gt;</code> component to the <code>/src/pages/blog/[slug].astro</code> template:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-html codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-html codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token comment">&lt;!-- /src/pages/blog/[slug].astro --&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">Layout</span><span class="token tag"> </span><span class="token tag attr-name">title</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value">{post.data.title}</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">ReadingProgressBar</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><br></span><span class="token-line"><span class="token plain">  </span><span class="token comment">&lt;!-- ... --&gt;</span><span class="token plain"></span><br></span><span class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">Layout</span><span class="token tag punctuation">&gt;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>The reading progress bar should now appear at the top of each blog post:</p>
<p><img loading="lazy" alt="Reading progress bar Web Component shown on top of a single post page, in orange color" src="https://www.debugbear.com/assets/images/reading-progress-bar-9ad7f11eba358f7c3b1eae304d11b9a2.png" width="1000" height="600" data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">7. Deploy the Coffee Blog to Cloudflare Workers<a href="https://www.debugbear.com/blog/astro-tutorial#7-deploy-the-coffee-blog-to-cloudflare-workers" class="hash-link" aria-label="Direct link to 7. Deploy the Coffee Blog to Cloudflare Workers" title="Direct link to 7. Deploy the Coffee Blog to Cloudflare Workers">​</a></h2>
<p>Before deploying the coffee blog to Cloudflare Workers, we need to <a href="https://docs.astro.build/en/guides/integrations-guide/cloudflare/" target="_blank" rel="noopener noreferrer">add the Cloudflare adapter</a>. To do so, run the following command in the terminal:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-bash codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-bash codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">npx astro add cloudflare</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>This single command installs the Cloudflare adapter, updates <code>astro.config.mjs</code>, and adds the <code>wrangler.jsonc</code> configuration file.</p>
<div class="theme-admonition theme-admonition-tip admonition_LMjb alert alert--success"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>tip</div><div class="admonitionContent_pGk6"><p>The installer will ask several yes/no questions about optional setup. Accept the defaults, except for the two prompts about generating Wrangler types: <code>generate-types</code> and adding <code>worker-configuration.d.ts</code> to <code>tsconfig.json</code>. Decline both in the name of the <a href="https://martinfowler.com/bliki/Yagni.html" target="_blank" rel="noopener noreferrer">YAGNI principle</a>, since this project doesn't use Cloudflare bindings or Workers APIs that require generated Wrangler types.</p></div></div>
<p>After installing the adapter, add a <code>"worker"</code> script to <code>"scripts"</code> in the <code>package.json</code> file so you can easily run the site using Cloudflare's local Workers runtime:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-json codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-json codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">"worker": "wrangler dev"</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>Then, run the following commands:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-bash codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-bash codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">npm run build</span><br></span><span class="token-line"><span class="token plain">npm run worker</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>The first one builds the site with the Cloudflare adapter. As part of the build, Astro generates the Wrangler configuration that <code>wrangler dev</code> uses to run the site locally.</p>
<p>The second command starts the built site using Cloudflare's local Workers runtime. The first time you run it, Wrangler will ask you to authorize access to your Cloudflare account. After authorization, it starts the Worker locally and provides a local URL where you can test the site.</p>
<p>If you want to preview the production build without the Cloudflare runtime, run the following commands instead, which use Astro's preview server rather than Cloudflare's local Workers runtime:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="language-bash codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-bash codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">npm run build</span><br></span><span class="token-line"><span class="token plain">npm run preview</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>Once you've verified that the site works locally, push the repository to GitHub or GitLab, connect it from the Cloudflare dashboard, follow the steps in Cloudflare's <strong>Create an app</strong> wizard, and deploy the coffee blog. For example, <a href="https://astro.perftuts.workers.dev/" target="_blank" rel="noopener noreferrer">here's mine</a>:</p>
<p><img loading="lazy" alt="A screenshot of the first screen of the Create an app wizard on the Cloudflare dashboard, showing 5 options - Continue with GitHub, Connect with GitLab, Start with Hello World!, Select a template, Upload your static files" src="https://www.debugbear.com/assets/images/cloudflare-dashboard-connect-github-gitlab-289c39c880da61d65ad67a74d036a22e.png" width="1000" height="418" data-abc="true" class="img_CujE"></p>
<p>From now on, whenever you push changes to your repository, Cloudflare will automatically redeploy your site.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">8. Test the Coffee Blog for Performance<a href="https://www.debugbear.com/blog/astro-tutorial#8-test-the-coffee-blog-for-performance" class="hash-link" aria-label="Direct link to 8. Test the Coffee Blog for Performance" title="Direct link to 8. Test the Coffee Blog for Performance">​</a></h2>
<p>Finally, let's see how Astro performs on the live site. You can use Lighthouse in Chrome DevTools or <a href="https://www.debugbear.com/test/website-speed" target="_blank" rel="noopener noreferrer">our free website speed test tool</a>.</p>
<p>Here are the results for the homepage in a DebugBear mobile test:</p>
<p><img loading="lazy" alt="Homepage mobile speed test results, lab metrics with the rendering filmstrip below it, results - Lab Score 100 percent, full TTFB 302 milliseconds, First Contentful Paint 478 milliseconds, Largest Contentful Paint 478 milliseconds, Total Blocking Time 0 milliseconds, Cumulative Layout Shift 0, Page Weight 2.49 kiloBytes - all got the green score " src="https://www.debugbear.com/assets/images/speed-test-homepage-lab-metrics-5cf61d08eb430231465e972770d43237.png" width="1200" height="1096" data-abc="true" class="img_CujE"></p>
<p>While these are strong results, keep in mind that the page doesn't contain any images or other media that could meaningfully affect its performance. For example, pulling in Sample APIs' coffee images for the live content collection could be a good next performance test.</p>
<p>If you click the <strong>Requests</strong> menu, you'll also see the <a href="https://www.debugbear.com/docs/waterfall">request waterfall chart</a>. It shows that the homepage makes just two requests: one for the HTML document and one for the favicon. There is no JavaScript to download, and the CSS is inlined in the HTML:</p>
<p><img loading="lazy" alt="Homepage mobile speed test results, request waterfall chart showing two HTTP requests for one HTML and one SVG files" src="https://www.debugbear.com/assets/images/speed-test-homepage-waterfall-de6d4d9bc3da63b53323d895045ac906.png" width="1200" height="617" data-abc="true" class="img_CujE"></p>
<p>Let's run another mobile test for one of the blog posts:</p>
<p><img loading="lazy" alt="A single blog post mobile speed test results, lab metrics with the rendering filmstrip below it, results - Lab Score 100 percent, full TTFB 319 milliseconds, First Contentful Paint 449 milliseconds, Largest Contentful Paint 449 milliseconds, Total Blocking Time 0 milliseconds, Cumulative Layout Shift 0, Page Weight 15.3 kiloBytes - all got the green score" src="https://www.debugbear.com/assets/images/speed-test-single-post-lab-metrics-1ccb651c74f932b6c2df8f0b684573a2.png" width="1200" height="1132" data-abc="true" class="img_CujE"></p>
<p>The scores are a little better compared to the homepage, even though the page also loads JavaScript here. Don't read too much into small differences in these lab measurements, however, as performance tests can slightly vary between runs.</p>
<p>A more useful comparison is the number and type of resources the browser has to load. If we navigate to the <strong>Requests</strong> menu, we can see that the blog post makes six requests in total, compared to just two for the homepage:</p>
<p><img loading="lazy" alt="A single blog post mobile speed test results, request waterfall chart showing six HTTP requests for one HTML, one SVG, and four JavaScript files" src="https://www.debugbear.com/assets/images/speed-test-single-post-waterfall-8dfc06d49d8962383086d8971a9b6a85.png" width="1200" height="787" data-abc="true" class="img_CujE"></p>
<p>Three of the four JavaScript requests come from Preact. And while the files are not large, the question still arises: do we really need to ship the Preact runtime just to add a Like button?</p>
<p>Not necessarily. We could easily implement the Like button as a Web Component instead, using the browser's built-in APIs. That would let us remove Preact from this component and avoid the framework runtime and its associated JavaScript requests. This is the approach I'd use on a real-world Astro site.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">Create Your Own Astro Site<a href="https://www.debugbear.com/blog/astro-tutorial#create-your-own-astro-site" class="hash-link" aria-label="Direct link to Create Your Own Astro Site" title="Direct link to Create Your Own Astro Site">​</a></h2>
<p>Use this tutorial as a starting point for your own Astro site, or keep improving the coffee blog.</p>
<p>For example, you could make the Like button persistent or rebuild it as a Web Component. You could also use the <code>tags</code> already in the frontmatter of the blog posts to add tag archive pages, or take the blog in a completely different direction <em>(e.g., Sample APIs provide <a href="https://sampleapis.com/api-list" target="_blank" rel="noopener noreferrer">many other APIs</a> as well)</em>.</p>
<p>You could also refurbish your own old blog with some brand-new features, such as a live RSS feed, a dynamic portfolio, a <a href="https://nownownow.com/about" target="_blank" rel="noopener noreferrer">/now page</a> with live content, or anything else you think could move the web forward.</p>
<p>If you make something worth sharing, send us the link by email to <code>matt [at] debugbear dot com</code> with the subject line "PerfTuts example"; we'll add selected examples to this article and let the tutorial grow with the community.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">See How Your Astro Site Performs for Real Users<a href="https://www.debugbear.com/blog/astro-tutorial#see-how-your-astro-site-performs-for-real-users" class="hash-link" aria-label="Direct link to See How Your Astro Site Performs for Real Users" title="Direct link to See How Your Astro Site Performs for Real Users">​</a></h2>
<p>You can also see how your Astro site performs from your visitors' perspective using DebugBear's <a href="https://www.debugbear.com/real-user-monitoring" target="_blank" rel="noopener noreferrer">real user monitoring (RUM)</a> feature.</p>
<p><a href="https://www.debugbear.com/signup" target="_blank" rel="noopener noreferrer">Sign up for our 14-day free trial</a> <em>(no credit card required)</em> and add the RUM script you can find at the bottom of the <strong>RUM &gt; Settings</strong> page to the <code>&lt;head&gt;</code> section of the <code>/src/layouts/Layout.astro</code> file:</p>
<p><img loading="lazy" alt="A screenshot of the RUM script in DebugBear" src="https://www.debugbear.com/assets/images/install-rum-analytics-6405437d0f7d5d0e050ae4a2dfdd27e7.png" width="1000" height="951" data-abc="true" class="img_CujE"></p>
<p>In addition to real user monitoring, you can also run lab tests under different scenarios from various test locations around the world.</p>
<p><a href="https://www.debugbear.com/signup" target="_blank" rel="noopener noreferrer">Get started with our 14-day free trial</a> so you can test your Astro site both proactively and in real time.</p>]]></content:encoded>
</item>

<item>
<title>Web-Perf Wednesday 012 – Return to the Unresolved Trace</title>
<link>https://csswizardry.com/2026/10/web-perf-wednesday-012-return-to-the-unresolved-trace/</link>
<guid isPermaLink="true">https://csswizardry.com/2026/10/web-perf-wednesday-012-return-to-the-unresolved-trace/</guid>
<description>A quiet week leaves room to revisit unfinished performance investigations and turn partial evidence into a clear next step.</description>
<pubDate>7 October 2026, 4:00 am</pubDate>
<content:encoded><![CDATA[A quiet week leaves room to revisit unfinished performance investigations and turn partial evidence into a clear next step.]]></content:encoded>
</item>

<item>
<title>Web-Perf Wednesday 011 – SpeedCurve’s INP Observer Missed Its Threshold</title>
<link>https://csswizardry.com/2026/09/web-perf-wednesday-011-speedcurves-inp-observer-missed-its-threshold/</link>
<guid isPermaLink="true">https://csswizardry.com/2026/09/web-perf-wednesday-011-speedcurves-inp-observer-missed-its-threshold/</guid>
<description>A long-standing lux.js bug left Event Timing on its 104 ms default, while browser releases improve testing and rendering performance.</description>
<pubDate>30 September 2026, 4:00 am</pubDate>
<content:encoded><![CDATA[A long-standing lux.js bug left Event Timing on its 104 ms default, while browser releases improve testing and rendering performance.]]></content:encoded>
</item>

<item>
<title>Example.com Just Launched The Biggest Redesign In Decades</title>
<link>https://www.debugbear.com/blog/example-dot-com-redesign-history</link>
<guid isPermaLink="true">https://www.debugbear.com/blog/example-dot-com-redesign-history</guid>
<description>A look at the history of visual changes to example.com.</description>
<pubDate>29 September 2026, 5:00 pm</pubDate>
<content:encoded><![CDATA[<p><code>example.com</code> is a reserved domain name set aside by the Internet Assigned Numbers Authority (IANA) for documentation purposes. It has been available as an example URL for developers and writers since the late nineties.</p>
<p>While the overall messaging of the page has been consistent over time, the visual appearance has received several major overhauls.</p>
<p>In this article, we take a look at the recent changes as well as the evolution of <code>example.com</code> over the last 20+ years.</p>
<!-- -->
<video class="videoPlayer_lp3F" width="1920" height="1080" controls="" playsinline="" autoplay="" muted="" loop="" preload="none"><source src="https://www.debugbear.com/dimg/example-redesign-history.mp4" type="video/mp4">Your browser does not support the video tag.</video>
<h2 class="anchor anchorWithStickyNavbar_FNw8">What changed on 28 September 2026?<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#what-changed-on-28-september-2026" class="hash-link" aria-label="Direct link to What changed on 28 September 2026?" title="Direct link to What changed on 28 September 2026?">​</a></h2>
<p>Until 28 September 2026, example.com was a static English-language page. The redesign introduces multi-language support using JavaScript, showing the content in a new language every 5 seconds.</p>
<p>Example.com shows an explanation of the site's purpose in English, Arabic, Chinese, French, Russian and Spanish. This is the English-language message:</p>
<blockquote>
<p>This domain is for use in documentation examples without needing permission. This is not a service, avoid relying on it for testing and monitoring purposes.</p>
</blockquote>
<p>Along with introducing multi-language support, the JavaScript code also inserts an SVG book icon.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/3fcf95b7dbb16b31bb844bc5db74cb5e.png" alt="Old versus new version of example.com" data-abc="true" class="img_CujE"></p>
<h3 class="anchor anchorWithStickyNavbar_FNw8">How does the language transition animation work?<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#how-does-the-language-transition-animation-work" class="hash-link" aria-label="Direct link to How does the language transition animation work?" title="Direct link to How does the language transition animation work?">​</a></h3>
<p>The new site doesn't just replace the content when showing the next language. Instead, there's a gradual opacity transition.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/639d7b9aca8f483d027ecf028afa3730.png" alt="Language transition" data-abc="true" class="img_CujE"></p>
<p>The opacity animation itself just uses <code>transition: opacity .4s;</code>. But how is the character-by-character transition implemented?</p>
<p>Each character is wrapped in its own <code>span</code> element, and each <code>span</code> gets a slightly larger <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/transition-delay" target="_blank" rel="noopener noreferrer">CSS <code>transition-delay</code></a> than the one before it. That means each character's opacity animation happens at a slightly different time.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/7ca8f04f11834c46652ba051d24c057e.png" alt="Chrome DevTools elements with transition delay" data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">More changes on 3 October 2026<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#more-changes-on-3-october-2026" class="hash-link" aria-label="Direct link to More changes on 3 October 2026" title="Direct link to More changes on 3 October 2026">​</a></h2>
<p>Example.com has now removed the animation and shows all languages right from the start:</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/597228b498d33055c12fb02955e09928.png" alt="Example.com showing all languages" data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">Statement from IANA<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#statement-from-iana" class="hash-link" aria-label="Direct link to Statement from IANA" title="Direct link to Statement from IANA">​</a></h2>
<p>IANA provided the following statement explaining the reasoning behind the recent changes:</p>
<blockquote>
<p>The changes we make to the site from time-to-time are usually informed by a desire to either reduce the overall bandwidth demands of serving the site, or improve its utility. At its heart it is a placeholder for a site no-one should be accessing, but we have historically showed a message there to inform visitors about why the domain is registered and how they may use it.</p>
<p>As it tends to be heavily trafficked, the overall bandwidth is a key consideration, and to that end one of the changes we made this week was to split the page content into a more basic page, augmented by a separate Javascript file with additional content beyond that. Since most of the traffic to the site is automated it tends not to fetch the Javascript file, reducing overall data requirements.</p>
<p>The fundamental purpose of the domain is for use in documentation, such as illustrative examples you may find in instructions. We know that people may, for example, cut-and-paste a sample configuration file and forget to customize entries that contain an example domain, and we expect we get some incidental traffic from that. However, the domain isn't intended to be a general purpose endpoint for things like availability testing so we do not encourage that kind of usage. There is no requirement there is a HTTP service present on the host in order to fulfill its purpose, we just operate it as a courtesy.</p>
</blockquote>
<h2 class="anchor anchorWithStickyNavbar_FNw8">January 20, 2002: What did the earliest version look like?<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#january-20-2002-what-did-the-earliest-version-look-like" class="hash-link" aria-label="Direct link to January 20, 2002: What did the earliest version look like?" title="Direct link to January 20, 2002: What did the earliest version look like?">​</a></h2>
<p>According to <a href="https://en.wikipedia.org/wiki/Example.com" target="_blank" rel="noopener noreferrer">Wikipedia</a>, the <code>example.com</code> website launched on January 1, 1999. The earliest version available to view in the Wayback Machine is from January 20, 2002.</p>
<p>The earliest version of the website only featured a list of domain names reserved by the Internet Corporation for Assigned Names and Numbers (ICANN) and IANA.</p>
<blockquote>
<p>In the .com, .net, and .org top-level domains (TLDs), various domain names are reserved. These names are reserved from initial registration, but to the extent they are already registered the existing registrant may renew them.</p>
</blockquote>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/58a7d948655cc07f3c6082ae068d2b58.png" alt="Example.com: January 20, 2002" data-abc="true" class="img_CujE"></p>
<p>Structurally, the HTML used a table layout, with the ICANN logo positioned in the top left corner of the page.</p>
<p>The server headers show that the site used Apache 1.3.22 at this point.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">March 28, 2002: simplified messaging<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#march-28-2002-simplified-messaging" class="hash-link" aria-label="Direct link to March 28, 2002: simplified messaging" title="Direct link to March 28, 2002: simplified messaging">​</a></h2>
<p>The first version of the <code>example.com</code> wording we recognize today came 67 days after the first available screenshot:</p>
<blockquote>
<p>You have reached this web page by typing "example.com", "example.net", or "example.org" into your web browser.</p>
</blockquote>
<blockquote>
<p>These domain names are reserved for use in documentation and are not available for registration.</p>
</blockquote>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/3dfccea58c8131bae0191eb3de3ccdab.png" alt="Example.com: March 28, 2002" data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">February 7, 2003: RFC 2606 citation added<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#february-7-2003-rfc-2606-citation-added" class="hash-link" aria-label="Direct link to February 7, 2003: RFC 2606 citation added" title="Direct link to February 7, 2003: RFC 2606 citation added">​</a></h2>
<p>In February 2003, the content was amended to include a link to <a href="https://www.rfc-editor.org/info/rfc2606/#section-3" target="_blank" rel="noopener noreferrer">RFC 2606</a>, the official document for reserved top-level and second-level domains, so visitors could find more information on the website's purpose.</p>
<p>There was also a small technical change around this time: the server was upgraded to Apache 1.3.27 on Red Hat Linux.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">July 30, 2010: .edu added to domain list<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#july-30-2010-edu-added-to-domain-list" class="hash-link" aria-label="Direct link to July 30, 2010: .edu added to domain list" title="Direct link to July 30, 2010: .edu added to domain list">​</a></h2>
<p>The website then went unchanged for seven years. On July 30, 2010, a slight change was made: <code>example.edu</code> was now pointed to the landing page, alongside the three previous domains.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">July 29, 2013: migration and landing page redesign<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#july-29-2013-migration-and-landing-page-redesign" class="hash-link" aria-label="Direct link to July 29, 2013: migration and landing page redesign" title="Direct link to July 29, 2013: migration and landing page redesign">​</a></h2>
<p>From January 2011, <code>example.com</code> didn't serve its own page. Instead, it redirected visitors to a page on IANA's website.</p>
<p>That changed on July 29, 2013, when the redirect was removed and <code>example.com</code> began serving its own page from the EdgeCast <a href="https://www.debugbear.com/blog/cdn" target="_blank" rel="noopener noreferrer">Content Delivery Network (CDN)</a> instead of Apache, alongside a major redesign of the landing page. The new design used a rounded card with an <code>&lt;h1&gt;</code> reading 'Example Domain' and a new description of the website's purpose.</p>
<p>The viewable domain list was now gone, with a link to IANA's website for more information.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/0fa2176503bcdb3aa6b9044d636a0c21.png" alt="Example.com: July 29, 2013" data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">October 17, 2019: font and copy changes<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#october-17-2019-font-and-copy-changes" class="hash-link" aria-label="Direct link to October 17, 2019: font and copy changes" title="Direct link to October 17, 2019: font and copy changes">​</a></h2>
<p>A new update came six years after the biggest change in the website's history. The layout remained the same, but the font stack was modernized to include more fallback fonts for devices such as Macs, iPhones, and Windows PCs.</p>
<p>The copy was tweaked slightly, with 'is for use in illustrative examples' replacing 'established to be used' and 'in literature' replacing 'in examples'.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/d6b8b3a027604b36f74b48a4527de28e.png" alt="Example.com: October 17, 2019" data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">January 15, 2025: infrastructure switch<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#january-15-2025-infrastructure-switch" class="hash-link" aria-label="Direct link to January 15, 2025: infrastructure switch" title="Direct link to January 15, 2025: infrastructure switch">​</a></h2>
<p>Almost twelve years after the switch from Apache to the EdgeCast CDN, another infrastructure change took place: the <code>Server</code> header disappeared entirely. The <a href="https://www.debugbear.com/docs/http-headers" target="_blank" rel="noopener noreferrer">response headers</a> alone don't reveal which platform was serving the page at this point.</p>
<p>A change of provider around this time isn't surprising. EdgeCast had been acquired from Yahoo by Limelight Networks in 2022 and rebranded as Edgio. Edgio then filed for Chapter 11 bankruptcy in late 2024, with Akamai acquiring select Edgio customer contracts.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">October 9, 2025: redesign<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#october-9-2025-redesign" class="hash-link" aria-label="Direct link to October 9, 2025: redesign" title="Direct link to October 9, 2025: redesign">​</a></h2>
<p>Later in 2025, the page was redesigned. The card design was dropped, and the HTML was minified. The copy was changed to a more direct message, again linking to IANA's website, with the link text changed from 'More information...' to 'Learn more'.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/6cf6b39d46f770ea59d904cc8c0a6373.png" alt="Example.com: October 9, 2025" data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">December 17, 2025: Cloudflare migration<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#december-17-2025-cloudflare-migration" class="hash-link" aria-label="Direct link to December 17, 2025: Cloudflare migration" title="Direct link to December 17, 2025: Cloudflare migration">​</a></h2>
<p>Less than a year after the previous switch, the infrastructure changed again: the site now serves from <a href="https://www.debugbear.com/blog/cdn#setting-up-cloudflare" target="_blank" rel="noopener noreferrer">Cloudflare</a>, as indicated by the <code>cloudflare</code> server header and <code>CF-RAY</code> header in the response. Whatever platform served the page after the January 2025 change, it has now been replaced with a Cloudflare-fronted setup.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">June 9, 2026: favicon and HTML tweaks<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#june-9-2026-favicon-and-html-tweaks" class="hash-link" aria-label="Direct link to June 9, 2026: favicon and HTML tweaks" title="Direct link to June 9, 2026: favicon and HTML tweaks">​</a></h2>
<p>The next update, on June 9, 2026, brought no visual changes. The only HTML changes on the page itself were a favicon tag being added and the <code>&lt;p&gt;</code> tags being closed. The page now includes an empty favicon:</p>
<pre tabindex="0" class="codeBlockStandalone_Nhx8 thin-scrollbar codeBlockContainer_aalF theme-code-block"><code class="codeBlockLines_RjmQ"><div class="codeBlockContainer_aalF theme-code-block"><div class="codeBlockContent_MHx8"><pre tabindex="0" class="prism-code language-text codeBlock_zHgq thin-scrollbar"><code class="codeBlockLines_RjmQ"><span class="token-line"><span class="token plain">&lt;link rel="icon" href="data:," /&gt;</span><br></span></code></pre><div class="buttonGroup_Sd8_"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_LnQD" aria-hidden="true"><svg viewbox="0 0 24 24" class="copyButtonIcon_t3l1"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewbox="0 0 24 24" class="copyButtonSuccessIcon_IiZV"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div></code></pre>
<p>This avoids a lot of unnecessary requests, as otherwise browsers look for a favicon at <code>/favicon.ico</code>.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/1cfc0dbb0685ecbfcdb078c0e845f4fe.png" alt="DebugBear showing the removed favicon request" data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">Avoid relying on it for testing and monitoring purposes<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#avoid-relying-on-it-for-testing-and-monitoring-purposes" class="hash-link" aria-label="Direct link to Avoid relying on it for testing and monitoring purposes" title="Direct link to Avoid relying on it for testing and monitoring purposes">​</a></h2>
<p>Example.com first introduced an "Avoid use in operations" message in 2025.</p>
<p>For developers, it's tempting to use <code>example.com</code> to check if the network is working or test HTTP requests. The example server likely receives a lot of traffic through this.</p>
<p>However, as the page now clarifies, it's not a service you can rely on. When we look at our <a href="https://www.debugbear.com/uptime-monitoring" target="_blank" rel="noopener noreferrer">uptime monitoring</a> data, we can see that every now and then the example.com server doesn't respond correctly.</p>
<p>Using infrastructure that your organization controls can be more reliable.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/ae3d696fc972adc2b908ec006d4abc4b.png" alt="Example.com failed uptime checks" data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">Conclusion<a href="https://www.debugbear.com/blog/example-dot-com-redesign-history#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion">​</a></h2>
<p>Looking at the visual timeline, we can see that changes to <code>example.com</code> happen sporadically. Sometimes a couple of small tweaks happen in back-to-back months. Other times the website remains untouched for periods of up to seven years.</p>
<p>We have been <a href="https://www.debugbear.com/project/3/pageLoad/15/vitals?cruxHistory=yes&amp;dateRange=last-3-months" target="_blank" rel="noopener noreferrer">monitoring the page</a> for several years in our public demo project. The <a href="https://www.debugbear.com/blog/chrome-user-experience-report" target="_blank" rel="noopener noreferrer">Chrome UX Report (CrUX) data</a> shows how page speed has changed over time.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/4d490599f6369e0dc9a7a268eecf8aae.png" alt="Example.com performance data" data-abc="true" class="img_CujE"></p>
<p>To learn more about how your website is performing, <a href="https://www.debugbear.com/signup" target="_blank" rel="noopener noreferrer">sign up</a> for a free 14-day DebugBear trial.</p>
<!-- -->
<div class="cta_Vy7l"><div class="ctaContainer_dzq8"><div class="ctaImage_MZ5b"><img src="https://www.debugbear.com/dimg/57001785e35454b95f43aa4f3bf6f857.png" alt="Illustration of website monitoring" class="themedComponent_mlkZ themedComponent--light_NVdE"><img src="https://www.debugbear.com/dimg/b2c0ae7a8dfbc4b18ba827a334c5c1af.png" alt="Illustration of website monitoring" class="themedComponent_mlkZ themedComponent--dark_xIcU"></div><div class="ctaContent_VgWi"><h2>Monitor Page Speed &amp; Core Web Vitals</h2><div class="ctaBenefits_OYw7"><h3>DebugBear monitoring includes:</h3><ul><li>In-depth Page Speed Reports</li><li>Automated Recommendations</li><li>Real User Analytics Data</li></ul></div><div><a href="https://www.debugbear.com/signup" class="ctaButton_Uos9 hidden-when-logged-in"><span>Start Free Trial</span></a><a href="https://www.debugbear.com/app" class="ctaButton_Uos9 visible-when-logged-in"><span>Go To App</span></a></div></div></div></div>]]></content:encoded>
</item>

<item>
<title>Lazy Loading Images and Media: How Far Ahead Browsers Fetch</title>
<link>https://calibreapp.com/blog/native-lazy-loading</link>
<guid isPermaLink="true">https://calibreapp.com/blog/native-lazy-loading</guid>
<description>How far ahead Chrome, Firefox and Safari fetch lazy media, why a lazy hero image slows your page, and where to use lazy, eager and fetchpriority.</description>
<pubDate>27 September 2026, 5:00 pm</pubDate>
<content:encoded><![CDATA[How far ahead Chrome, Firefox and Safari fetch lazy media, why a lazy hero image slows your page, and where to use lazy, eager and fetchpriority.]]></content:encoded>
</item>

<item>
<title>Web-Perf Wednesday 010 – Safari Keeps Scrolled Content in Place</title>
<link>https://csswizardry.com/2026/09/web-perf-wednesday-010-safari-keeps-scrolled-content-in-place/</link>
<guid isPermaLink="true">https://csswizardry.com/2026/09/web-perf-wednesday-010-safari-keeps-scrolled-content-in-place/</guid>
<description>Safari 27 adds scroll anchoring, tighter HTTP caching, and static Service Worker routes, while Chrome improves performance tooling.</description>
<pubDate>23 September 2026, 4:00 am</pubDate>
<content:encoded><![CDATA[Safari 27 adds scroll anchoring, tighter HTTP caching, and static Service Worker routes, while Chrome improves performance tooling.]]></content:encoded>
</item>

<item>
<title>“AI, make the website good”</title>
<link>https://www.zachleat.com/web/ai-websites/</link>
<guid isPermaLink="true">https://www.zachleat.com/web/ai-websites/</guid>
<description>
&lt;p&gt;Millions of &lt;em&gt;Large-Language-Model&lt;/em&gt;-generated words have been spilled on craftsmanship in this &lt;strong&gt;gilded-age of software development&lt;/strong&gt;. We now have agents to plan how code will be written, agents to write code, agents to review code, agents to orchestrate and dispatch tasks to other more (or less) capable subagents (these heavily subsidized tokens aren’t going to pay for themselves).&lt;/p&gt;

&lt;p&gt;I’ve been in this game long enough to recognize tooling improvements from software companies at the bleeding edge of technique and application and it usually goes something like this: “If we make it easier for developers to create software, they will have more bandwidth to produce higher quality software!”&lt;/p&gt;

&lt;p&gt;I’m always left wondering (suspecting the answer with hopeful optimism that my hunch is wrong this time): do these workflow improvements actually improve how people (technical or otherwise) &lt;em&gt;experience&lt;/em&gt; software? Certainly &lt;strong&gt;we have more software&lt;/strong&gt;. But is it better? I don’t know.&lt;/p&gt;

&lt;p&gt;Front-end web developers have lived through a few cycles of this (seven or eight years of client rendering, fifteen years of the &lt;em&gt;single-page-application&lt;/em&gt;-era smokescreen are a few that come to mind), sold as though improvements to developer experience would cause the reanimated corpse of Ronald Reagan himself to ask you for commit access to ship trickle-down improvements to user experience.&lt;/p&gt;
&lt;p&gt;Is this cycle repeating the same playbook (on a more accelerated pace)? Certainly we’ve seen numerous recent anecdotal examples of software becoming &lt;em&gt;less&lt;/em&gt; reliable over time (GitHub is the most recent scapegoat I’ve seen, but on my read they seem to be a second-order victim more than anything).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(Seems like a great time to be in the Monitoring/Status Page business)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With these seismic changes, I will continue to make the argument that craft defined as &lt;strong&gt;caring about the quality of output&lt;/strong&gt; (independent of the tools used to generate the output) is &lt;strong&gt;more important than ever&lt;/strong&gt;. Caring about what you ship will always have &lt;strong&gt;real-world value&lt;/strong&gt; to &lt;s&gt;visitors/consumers/users&lt;/s&gt; &lt;strong&gt;real people&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Prefers-reduced-motion respecting web performance filmstrips from &lt;a href=&quot;https://www.speedlify.dev/group/ai/&quot;&gt;the AI category on Speedlify&lt;/a&gt;, a category boasting a failing median Core Web Vitals score with 12 out of 13 sites failing Lighthouse performance and Axe accessibility checks.&lt;/em&gt;&lt;/p&gt;</description>
<pubDate>20 September 2026, 10:00 pm</pubDate>
<content:encoded><![CDATA[
<p>Millions of <em>Large-Language-Model</em>-generated words have been spilled on craftsmanship in this <strong>gilded-age of software development</strong>. We now have agents to plan how code will be written, agents to write code, agents to review code, agents to orchestrate and dispatch tasks to other more (or less) capable subagents (these heavily subsidized tokens aren’t going to pay for themselves).</p>

<p>I’ve been in this game long enough to recognize tooling improvements from software companies at the bleeding edge of technique and application and it usually goes something like this: “If we make it easier for developers to create software, they will have more bandwidth to produce higher quality software!”</p>

<p>I’m always left wondering (suspecting the answer with hopeful optimism that my hunch is wrong this time): do these workflow improvements actually improve how people (technical or otherwise) <em>experience</em> software? Certainly <strong>we have more software</strong>. But is it better? I don’t know.</p>

<p>Front-end web developers have lived through a few cycles of this (seven or eight years of client rendering, fifteen years of the <em>single-page-application</em>-era smokescreen are a few that come to mind), sold as though improvements to developer experience would cause the reanimated corpse of Ronald Reagan himself to ask you for commit access to ship trickle-down improvements to user experience.</p>
<p>Is this cycle repeating the same playbook (on a more accelerated pace)? Certainly we’ve seen numerous recent anecdotal examples of software becoming <em>less</em> reliable over time (GitHub is the most recent scapegoat I’ve seen, but on my read they seem to be a second-order victim more than anything).</p>
<p><em>(Seems like a great time to be in the Monitoring/Status Page business)</em></p>

<p>With these seismic changes, I will continue to make the argument that craft defined as <strong>caring about the quality of output</strong> (independent of the tools used to generate the output) is <strong>more important than ever</strong>. Caring about what you ship will always have <strong>real-world value</strong> to <s>visitors/consumers/users</s> <strong>real people</strong>.</p>
<p><em>Prefers-reduced-motion respecting web performance filmstrips from <a href="https://www.speedlify.dev/group/ai/">the AI category on Speedlify</a>, a category boasting a failing median Core Web Vitals score with 12 out of 13 sites failing Lighthouse performance and Axe accessibility checks.</em></p>]]></content:encoded>
</item>

<item>
<title>Web-Perf Wednesday 009 – CrUX Makes Ad Weight Public</title>
<link>https://csswizardry.com/2026/09/web-perf-wednesday-009-crux-makes-ad-weight-public/</link>
<guid isPermaLink="true">https://csswizardry.com/2026/09/web-perf-wednesday-009-crux-makes-ad-weight-public/</guid>
<description>CrUX adds public ad count, density, CPU, and network metrics, giving publishers and advertisers a shared view of real ad weight.</description>
<pubDate>16 September 2026, 4:00 am</pubDate>
<content:encoded><![CDATA[CrUX adds public ad count, density, CPU, and network metrics, giving publishers and advertisers a shared view of real ad weight.]]></content:encoded>
</item>

<item>
<title>Icon Fall Web Component</title>
<link>https://www.zachleat.com/web/icon-fall/</link>
<guid isPermaLink="true">https://www.zachleat.com/web/icon-fall/</guid>
<description>&lt;p&gt;I’ve been doing some spring/fall cleaning and found an unpublished follow-up component to &lt;a href=&quot;https://www.zachleat.com/web/snow-fall/&quot;&gt;&lt;code&gt;&amp;lt;snow-fall&amp;gt;&lt;/code&gt;&lt;/a&gt; (a web component to “snow” on your website — which also happens to work with emoji too). Originally it was created as an easter egg for &lt;a href=&quot;https://blog.fontawesome.com/&quot;&gt;Blog Awesome&lt;/a&gt; (click on the Font Awesome flag in the very bottom of the footer) but others have wanted to use it for more things!&lt;/p&gt;
&lt;p&gt;Presenting: &lt;code&gt;&amp;lt;icon-fall&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zachleat.github.io/icon-fall/demo.html&quot;&gt;Demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zachleat/icon-fall&quot;&gt;Source code on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s in use on this page (if you are on my web site and not in a feed reader). You can also install it with &lt;code&gt;npm install @zachleat/icon-fall&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It has options for &lt;code&gt;count&lt;/code&gt; (attribute, number of icons) and a comma separated list of colors (via &lt;code&gt;--icon-fall-colors&lt;/code&gt; custom property).&lt;/p&gt;

&lt;is-land on:media=&quot;(prefers-reduced-motion: no-preference)&quot;&gt;
	&lt;icon-fall count=&quot;20&quot;&gt;
		&lt;svg class=&quot;z-icon&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewbox=&quot;0 0 512 512&quot;&gt;&lt;use href=&quot;#fab-fa-font-awesome&quot; xlink:href=&quot;#fab-fa-font-awesome&quot;&gt;&lt;/use&gt;&lt;/svg&gt;
		&lt;svg class=&quot;z-icon&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewbox=&quot;0 0 640 512&quot;&gt;&lt;use href=&quot;#fab-fa-web-awesome&quot; xlink:href=&quot;#fab-fa-web-awesome&quot;&gt;&lt;/use&gt;&lt;/svg&gt;
		&lt;svg class=&quot;z-icon&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewbox=&quot;0 0 448 512&quot;&gt;&lt;use href=&quot;#fab-fa-build-awesome&quot; xlink:href=&quot;#fab-fa-build-awesome&quot;&gt;&lt;/use&gt;&lt;/svg&gt;
	&lt;/icon-fall&gt;
&lt;/is-land&gt;</description>
<pubDate>15 September 2026, 10:00 pm</pubDate>
<content:encoded><![CDATA[<p>I’ve been doing some spring/fall cleaning and found an unpublished follow-up component to <a href="https://www.zachleat.com/web/snow-fall/"><code>&lt;snow-fall&gt;</code></a> (a web component to “snow” on your website — which also happens to work with emoji too). Originally it was created as an easter egg for <a href="https://blog.fontawesome.com/">Blog Awesome</a> (click on the Font Awesome flag in the very bottom of the footer) but others have wanted to use it for more things!</p>
<p>Presenting: <code>&lt;icon-fall&gt;</code>:</p>
<ul>
<li><a href="https://zachleat.github.io/icon-fall/demo.html">Demo</a></li>
<li><a href="https://github.com/zachleat/icon-fall">Source code on GitHub</a></li>
</ul>
<p>It’s in use on this page (if you are on my web site and not in a feed reader). You can also install it with <code>npm install @zachleat/icon-fall</code>.</p>
<p>It has options for <code>count</code> (attribute, number of icons) and a comma separated list of colors (via <code>--icon-fall-colors</code> custom property).</p>

<is-land on:media="(prefers-reduced-motion: no-preference)">
	<icon-fall count="20">
		<svg class="z-icon" width="1em" height="1em" viewbox="0 0 512 512"><use href="#fab-fa-font-awesome" xlink:href="#fab-fa-font-awesome"></use></svg>
		<svg class="z-icon" width="1em" height="1em" viewbox="0 0 640 512"><use href="#fab-fa-web-awesome" xlink:href="#fab-fa-web-awesome"></use></svg>
		<svg class="z-icon" width="1em" height="1em" viewbox="0 0 448 512"><use href="#fab-fa-build-awesome" xlink:href="#fab-fa-build-awesome"></use></svg>
	</icon-fall>
</is-land>]]></content:encoded>
</item>

<item>
<title>New Data From Google: See How Ads Impact Web Performance</title>
<link>https://www.debugbear.com/blog/chrome-ad-performance-insights</link>
<guid isPermaLink="true">https://www.debugbear.com/blog/chrome-ad-performance-insights</guid>
<description>Google has been working on new metrics to measure how ads impact performance. Learn how you can view this data for real visitors and how to check it yourself in Chrome DevTools.</description>
<pubDate>15 September 2026, 5:00 pm</pubDate>
<content:encoded><![CDATA[<p>Ads often have <a href="https://www.debugbear.com/blog/chrome-extensions-website-performance#the-impact-of-ad-blocking-on-website-performance" target="_blank" rel="noopener noreferrer">a big negative impact on web performance</a>. They frequently require large downloads and heavy CPU processing.</p>
<p>Yesterday, Google introduced new performance metrics to surface how ads impact user experience. You can find real-user data for these metrics in the Chrome User Experience Report, or check them yourself in Chrome DevTools.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">New ad metrics in Google's CrUX report<a href="https://www.debugbear.com/blog/chrome-ad-performance-insights#new-ad-metrics-in-googles-crux-report" class="hash-link" aria-label="Direct link to New ad metrics in Google's CrUX report" title="Direct link to New ad metrics in Google's CrUX report">​</a></h2>
<p>The <a href="https://www.debugbear.com/blog/chrome-user-experience-report">Chrome User Experience Report</a> provides real-world performance data for millions of websites, based on aggregated Chrome user data. Most prominently, CrUX reports the three <a href="https://www.debugbear.com/docs/metrics/core-web-vitals">Core Web Vitals</a> metrics that tell you how fast your website loads, how quickly it responds to user interactions, and whether the page layout is stable.</p>
<p>Google has now introduced four additional metrics focusing on ad performance:</p>
<ol>
<li>Ad Count: How many ads are displayed at once?</li>
<li>Ad Density: On average, how much of the page is covered by ads?</li>
<li>Ad CPU Weight: How much CPU processing time is caused by ad frames?</li>
<li>Ad Network Weight: How much data is loaded by ad frames?</li>
</ol>
<p>You can use tools like our <a href="https://www.debugbear.com/test/website-speed" target="_blank" rel="noopener noreferrer">website speed test</a> or Google's <a href="https://cruxvis.withgoogle.com/#/" target="_blank" rel="noopener noreferrer">CrUX Vis</a> tool to access this data.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/b08d95749222cc940a8c590ae24470ea.png" alt="Ad performance metrics" data-abc="true" class="img_CujE"></p>
<p>These new metrics provide insights into the impact of ads that previously weren't available. Publishers and ad networks can use this data to assess and reduce web performance issues caused by online adverts.</p>
<p>We can also use these metrics to create rankings showing how much the performance cost of ads varies across websites. For example, <a href="https://www.debugbear.com/project/97407/crux-trends?metrics%5B%5D=ad_count&amp;metrics%5B%5D=ad_density&amp;metrics%5B%5D=ad_kilobytes&amp;metrics%5B%5D=ad_cpu&amp;sortColumn=-experimental_ad_cpu" target="_blank" rel="noopener noreferrer">this dashboard</a> shows a ranking of news websites based on how much extra CPU processing ads are causing.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/8a28d60bfade56599776316e4f34ca7d.png" alt="Ad performance ranking for news websites. USA Today ads cause 11 seconds of CPU time per visit." data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">What are the new metrics?<a href="https://www.debugbear.com/blog/chrome-ad-performance-insights#what-are-the-new-metrics" class="hash-link" aria-label="Direct link to What are the new metrics?" title="Direct link to What are the new metrics?">​</a></h2>
<p>Let's take a closer look at the new metrics and what each one measures.</p>
<h3 class="anchor anchorWithStickyNavbar_FNw8">Ad Count<a href="https://www.debugbear.com/blog/chrome-ad-performance-insights#ad-count" class="hash-link" aria-label="Direct link to Ad Count" title="Direct link to Ad Count">​</a></h3>
<p>This metric tells you how many ad elements are visible on the page. An ad element could be an iframe, an image, or a video. Chrome checks this count every second and reports the average.</p>
<p>For example, we can see 4 ads in this screenshot:</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/24e9e1dc4eb013265fbca6aebdce8871.png" alt="USA Today page with 4 ads" data-abc="true" class="img_CujE"></p>
<p>So if a user sees 1 ad for 10 seconds and then 2 ads for 30 seconds, the ad count for that visit would be <code>(1 * 10 + 2 * 30) / (10 + 30) = 1.75</code>.</p>
<h3 class="anchor anchorWithStickyNavbar_FNw8">Ad Density<a href="https://www.debugbear.com/blog/chrome-ad-performance-insights#ad-density" class="hash-link" aria-label="Direct link to Ad Density" title="Direct link to Ad Density">​</a></h3>
<p>While the number of ads on a page is interesting, for the user it's more important how much of the page consists of ads and how much consists of the content they are interested in.</p>
<p>Ad Density measures what percentage of the page is covered by ad frames. For example, here 27% of the viewport is covered in ads.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/53007013204c463f0e856763e356175e.png" alt="Ads highlighted in Chrome" data-abc="true" class="img_CujE"></p>
<p>Like ad count, this information is sampled every second and Chrome then reports an average.</p>
<h3 class="anchor anchorWithStickyNavbar_FNw8">Ad Weight: CPU<a href="https://www.debugbear.com/blog/chrome-ad-performance-insights#ad-weight-cpu" class="hash-link" aria-label="Direct link to Ad Weight: CPU" title="Direct link to Ad Weight: CPU">​</a></h3>
<p>Ads don't just take up space but also require CPU processing to load and update. The CPU weight metric reports how much time the page spends running JavaScript code in ad frames. JavaScript execution of ad scripts in the main frame is not counted.</p>
<p>If we <a href="https://www.debugbear.com/blog/devtools-performance">collect a CPU profile</a> on a news website, we can see that the page consists of a large number of iframes. Most of them load and execute a meaningful amount of JavaScript code.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/4123590cd0d5318bffe31c581d5c1e96.png" alt="CPU profile showing the main frame and many subframes" data-abc="true" class="img_CujE"></p>
<div class="theme-admonition theme-admonition-info admonition_LMjb alert alert--info"><div class="admonitionHeading_GGQ4"><span class="admonitionIcon_ifdW"><svg viewbox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</div><div class="admonitionContent_pGk6"><p>Unlike ad count and density, ad CPU weight is reported across the entire lifetime of the page, rather than looking at an average.</p></div></div>
<h3 class="anchor anchorWithStickyNavbar_FNw8">Ad Weight: Network<a href="https://www.debugbear.com/blog/chrome-ad-performance-insights#ad-weight-network" class="hash-link" aria-label="Direct link to Ad Weight: Network" title="Direct link to Ad Weight: Network">​</a></h3>
<p>Finally, the network ad weight metric reports how much data ads are loading. You can see how many kilobytes or megabytes need to be transferred.</p>
<p>Like CPU weight, network weight is aggregated throughout the lifetime of the page.</p>
<p>This screenshot of the Chrome DevTools <a href="https://www.debugbear.com/blog/devtools-network">Network tab</a> shows which ad-related resources are downloaded on the page.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/25d730908f94a3f4fe1c5094eea0be10.png" alt="Chrome Network tab with network requests" data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">How do the ad performance metrics work?<a href="https://www.debugbear.com/blog/chrome-ad-performance-insights#how-do-the-ad-performance-metrics-work" class="hash-link" aria-label="Direct link to How do the ad performance metrics work?" title="Direct link to How do the ad performance metrics work?">​</a></h2>
<p>Google's CrUX report is based on data from logged-in Chrome users who have opted in to analytics reporting. Chrome measures these ad performance metrics and reports them back to Google servers.</p>
<p>If a website receives enough data reports to preserve user privacy, Google then makes aggregate metric data for the website publicly available.</p>
<p>There are three important factors to keep in mind when interpreting the final reported value:</p>
<ol>
<li>Google reports the <a href="https://www.debugbear.com/docs/rum/percentiles">75th percentile</a> value. Most visitors have a better experience, but 25% of visitors experience a worse impact than shown by the metric.</li>
<li>Only visits with ads are considered. A user with an ad blocker would be excluded from aggregated scores.</li>
<li>CrUX data is aggregated over a 28-day period. Any changes you make will take some time to show up in the metrics.</li>
</ol>
<h3 class="anchor anchorWithStickyNavbar_FNw8">What websites are included?<a href="https://www.debugbear.com/blog/chrome-ad-performance-insights#what-websites-are-included" class="hash-link" aria-label="Direct link to What websites are included?" title="Direct link to What websites are included?">​</a></h3>
<p>CrUX only reports ad metrics for websites that show third-party ads and have an <code>ads.txt</code> (Authorized Digital Sellers) file.</p>
<p>In general, websites need to be indexable and somewhat popular before they are included in the CrUX dataset.</p>
<h3 class="anchor anchorWithStickyNavbar_FNw8">How does Google detect ad frames and resources?<a href="https://www.debugbear.com/blog/chrome-ad-performance-insights#how-does-google-detect-ad-frames-and-resources" class="hash-link" aria-label="Direct link to How does Google detect ad frames and resources?" title="Direct link to How does Google detect ad frames and resources?">​</a></h3>
<p>Chrome detects whether an iframe is an ad by looking at the document URL. It uses a subset of <a href="https://easylist.to/" target="_blank" rel="noopener noreferrer">EasyList</a> to decide if the URL belongs to an ad network.</p>
<p>Individual resources outside of an ad frame can also be detected as ad-related. They will then contribute to the ad network weight metric.</p>
<p>In addition to checking the URL of requests, Chrome also looks at the JavaScript stack to decide if an ad script is requesting an additional ad-related resource.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">Ad performance reporting in Chrome DevTools<a href="https://www.debugbear.com/blog/chrome-ad-performance-insights#ad-performance-reporting-in-chrome-devtools" class="hash-link" aria-label="Direct link to Ad performance reporting in Chrome DevTools" title="Direct link to Ad performance reporting in Chrome DevTools">​</a></h2>
<p>In addition to the CrUX metrics, Google is also introducing a new Ads reporting panel in Chrome DevTools.</p>
<p>You can try it in Chrome Canary right now. Just <a href="https://www.debugbear.com/blog/use-chrome-devtools">open DevTools</a>, select the <strong>Application</strong> tab and then open the <strong>Ads</strong> UI from the sidebar.</p>
<p>Here you can see the values of the four ad performance metrics as well as a list of iframes and scripts that impact the page.</p>
<p>I've also ticked the <strong>Highlight ads</strong> checkbox to add the red highlight for each ad frame.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/33bb2f0640336e1945dc990a4b7f97b8.png" alt="Chrome DevTools ad reporting UI" data-abc="true" class="img_CujE"></p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">What makes this new CrUX data especially interesting?<a href="https://www.debugbear.com/blog/chrome-ad-performance-insights#what-makes-this-new-crux-data-especially-interesting" class="hash-link" aria-label="Direct link to What makes this new CrUX data especially interesting?" title="Direct link to What makes this new CrUX data especially interesting?">​</a></h2>
<p>Publishers can use <a href="https://www.debugbear.com/docs/what-is-real-user-monitoring">real user monitoring</a> tools to get insight into the performance of content that's hosted on their own domain. But for security and privacy reasons, these tools can't access performance data from within iframes, which is how ads are most commonly embedded on websites.</p>
<p>Google's new data changes that: it's now possible to view real user data on how ads impact user experience. Lab tests with tools like Lighthouse can provide some insights on this, but they don't correctly reflect real-world usage and interaction patterns or the types of devices visitors are using.</p>
<h2 class="anchor anchorWithStickyNavbar_FNw8">How do ads impact Core Web Vitals?<a href="https://www.debugbear.com/blog/chrome-ad-performance-insights#how-do-ads-impact-core-web-vitals" class="hash-link" aria-label="Direct link to How do ads impact Core Web Vitals?" title="Direct link to How do ads impact Core Web Vitals?">​</a></h2>
<p>Google's ad performance metrics show whether ads are using CPU and network resources. But does this extra activity actually take resources away from the main page content and cause worse Core Web Vitals scores?</p>
<p>We can set up a <a href="https://www.debugbear.com/docs/dashboards">custom dashboard</a> in DebugBear and add a scatter plot to find out.</p>
<p>This screenshot shows that websites with more ad activity tend to have worse Interaction to Next Paint scores.</p>
<p><img loading="lazy" src="https://www.debugbear.com/dimg/97e6f95c4d6be8f502d521db927a93b1.png" alt="Scatter chart showing a correlation between ad CPU activity and INP" data-abc="true" class="img_CujE"></p>
<!-- -->
<div class="cta_Vy7l"><div class="ctaContainer_dzq8"><div class="ctaImage_MZ5b"><img src="https://www.debugbear.com/dimg/57001785e35454b95f43aa4f3bf6f857.png" alt="Illustration of website monitoring" class="themedComponent_mlkZ themedComponent--light_NVdE"><img src="https://www.debugbear.com/dimg/b2c0ae7a8dfbc4b18ba827a334c5c1af.png" alt="Illustration of website monitoring" class="themedComponent_mlkZ themedComponent--dark_xIcU"></div><div class="ctaContent_VgWi"><h2>Monitor Page Speed &amp; Core Web Vitals</h2><div class="ctaBenefits_OYw7"><h3>DebugBear monitoring includes:</h3><ul><li>In-depth Page Speed Reports</li><li>Automated Recommendations</li><li>Real User Analytics Data</li></ul></div><div><a href="https://www.debugbear.com/signup" class="ctaButton_Uos9 hidden-when-logged-in"><span>Start Free Trial</span></a><a href="https://www.debugbear.com/app" class="ctaButton_Uos9 visible-when-logged-in"><span>Go To App</span></a></div></div></div></div>]]></content:encoded>
</item>

</channel>
</rss>