The web framework for content sites: HTML made ahead of time, JavaScript only where things move.
Astro turns components and Markdown into fast websites: pages are built to plain HTML, and only the interactive parts (islands) send JavaScript to the browser. This course starts at what a website is and climbs, one small step at a time, to routing, content collections, islands, on-demand rendering, actions, caching, security and integrations, building a personal blog with a recipe section along the way.
What you’ll be able to do
Explain how a page gets from a server to a browser, and what Astro does at build time, per request and in the browser.
Build a site from .astro components, props, slots, layouts and scoped styles, with file-based and dynamic routes.
Publish Markdown through typed content collections, with optimised images and fonts.
Add interactive islands with the right client directive, and keep the rest of the page free of JavaScript.
Mix prerendered and on-demand routes with adapters, endpoints, middleware, actions, i18n and view transitions.
Ship a fast, secure site: caching, a Content Security Policy, deployment, integrations, and reasons for each choice.
Modules
How a website reaches you· 9 slides
Tools, frameworks and the build step· 9 slides
Your first Astro project· 10 slides
Inside an .astro file· 9 slides
Components and props· 9 slides
Slots: components that wrap other things· 10 slides
Styles and small scripts· 10 slides
Pages, links and routes· 10 slides
Layouts and the page head· 9 slides
Markdown and content collections· 10 slides
Images, fonts and other files· 10 slides
Islands and client directives· 10 slides
Rendering modes, adapters and deploying· 10 slides
Endpoints, middleware and environment variables· 10 slides
Forms and actions· 10 slides
Languages and view transitions· 10 slides
Performance and caching· 10 slides
Security and the Content Security Policy· 10 slides
Integrations, the build pipeline and architecture· 9 slides
Mastery challenge· 9 slides
Final project: a conference website· 19 slides
Interactive parts (exercises, code to run, walkthroughs, flashcards, checklists) are drawn still and marked “Interactive · disabled in the PDF”. Do them in the app; the answers are at the end.
Module 1: How a website reaches you
Astro1 / 212
Module 1 · How a website reaches you
A website is files that one computer sends to another
When you open a recipe blog on your phone, two computers are involved. One of them, somewhere on the internet, holds the blog’s files and waits to be asked for them. The other is your phone, which asks, receives the files, and draws them on the screen.
Server
A computer (or a program on one) that waits for requests and answers them. It serves the website.
Browser
The program you use to visit websites: Chrome, Safari, Firefox, Edge. It asks servers for pages and draws them.
Client
Whoever asks. On the web, the client is usually a browser.
Nothing magic. A server is an ordinary computer, often in a data centre, running a program that answers requests. When you build a website with Astro, you are preparing what that program sends.
Astro2 / 212
Module 1 · How a website reaches you
The browser asks, the server answers
Every visit is a conversation with two halves. The browser sends a request ("please give me /about"); the server sends back a response (the page, or an error). The rules for that conversation are called HTTP.
One page visit: a request, a response, then more requests for the files the page needs.
Request
A message from the client: a method (like GET), an address, and some headers.
Response
The server’s answer: a status code (like 200 for OK, 404 for not found), headers, and a body (the content).
HTTP
The set of rules browsers and servers follow to talk to each other.
Astro3 / 212
Module 1 · How a website reaches you
An address says which server, and which page
Text
1https://example.com/recipes/pancakes?serves=2
Part
In the example
What it means
Scheme
https://
How to talk: HTTP, encrypted (the s is for secure)
Domain
example.com
Which server to ask
Path
/recipes/pancakes
Which page on that server
Query
?serves=2
Extra details for the page, as name=value pairs
The whole thing is a URL (Uniform Resource Locator), or simply an address. The path is the part you will shape most in Astro: each page you write gets one.
QuizInteractive · disabled in the PDF
In https://example.com/blog/first-post?ref=home, what is the path?
In https://example.com/blog/first-post?ref=home, what is the path?
Astro4 / 212
Module 1 · How a website reaches you
HTML, CSS and JavaScript each do one job
A browser understands three languages. HTML says what is on the page: headings, paragraphs, links, images. CSS says how it looks: colours, sizes, layout. JavaScript (JS) makes it do things after it loads: open a menu, filter a list.
about.html
1<html>2<head>3<title>About me</title>4<linkrel="stylesheet"href="/style.css"/>5</head>6<body>7<h1>Hi, I’m Sam</h1>8<p>I write about bread, soup and slow Sundays.</p>9<ahref="/recipes">See my recipes</a>10</body>11</html>
HTML is made of elements: an opening tag like <p>, content, and a closing tag like </p>.
Element
One piece of HTML, like <h1>Hi</h1>: a tag, its content, and its closing tag.
Attribute
Extra information on a tag, written name="value": href="/recipes".
`<head>` and `<body>`
The head holds facts about the page (its title, its CSS); the body holds what you see.
Astro5 / 212
Module 1 · How a website reaches you
The browser reads the HTML, then fetches what it points to
The first response is the HTML document.
The browser reads it and finds links to other files.
It asks the server for each one: stylesheets, pictures, fonts.
With HTML and CSS, it can draw the page you see.
JavaScript is downloaded and run too. Until it runs, anything it controls does nothing.
HTML first, then everything it links to.
Put in orderInteractive · disabled in the PDF
Put a first visit to a page in order.
1.You type an address and press Enter
2.The browser sends a request to the server
3.The browser asks for the CSS and images the HTML links to
4.The server answers with HTML
5.The page appears on screen
Astro6 / 212
Module 1 · How a website reaches you
Static pages are made ahead; dynamic pages are made per visit
Static page
An HTML file made before anyone visits
Everyone gets the same file
The server only has to send it: fast and cheap
Good for: a blog post, a recipe, an about page
Dynamic page
The HTML is made when the request arrives
Each visitor can get something different
The server runs code for every visit
Good for: your account, a cart, search results
Most of a typical site can be static. A recipe doesn’t change between one reader and the next, so there is no reason to make it again for every visit.
QuizInteractive · disabled in the PDF
Which page needs to be dynamic (made per visit)?
Which page needs to be dynamic (made per visit)?
Astro7 / 212
Module 1 · How a website reaches you
Every bit of JavaScript costs the visitor time
HTML and CSS only need to be read to be drawn. JavaScript must be downloaded, read and run before it does anything, and on a cheap phone on a slow connection that can take seconds. Many sites send JavaScript for things that never move, like an article’s text.
A useful habit. For each part of a page, ask: does this need to do something after it loads? A heading, a paragraph, a picture: no. A search box that filters as you type: yes. Only the second needs JavaScript.
QuizInteractive · disabled in the PDF
Which parts of a recipe page need JavaScript? Pick all that apply.
Which parts of a recipe page need JavaScript? Pick all that apply.
Astro8 / 212
Module 1 · How a website reaches you
Astro makes HTML ahead of time, and adds JavaScript only where you ask
Astro is a tool for building websites. You write pages from reusable pieces, and Astro turns them into plain HTML files before anyone visits. Where a part of the page must react to the reader, you mark it, and only that part sends JavaScript. Astro calls those parts islands.
Header and navigationHTML, no JavaScript
Recipe text and photoHTML, no JavaScript
Search boxan island: HTML plus a little JavaScript
FooterHTML, no JavaScript
A recipe page: mostly static HTML, with one island.
And when you need dynamic pages. Astro can also make pages per request, on a server, for the parts of a site that need it. You will get there in the advanced modules. Static is the default because it is the simplest and the fastest.
Astro9 / 212
Module 1 · How a website reaches you
Recap: how a website reaches you
A browser (the client) sends a request; a server sends a response.
A URL names the server (domain) and the page (path), with optional extras (query).
HTML is what is on the page, CSS how it looks, JavaScript what it does after loading.
Static pages are made ahead of time and shared by everyone; dynamic pages are made per visit.
JavaScript costs download and run time, so send it only where something must react.
Astro builds static HTML by default and adds JavaScript only in islands.
FlashcardsInteractive · disabled in the PDF
Server
A computer or program that waits for requests and answers them.
Browser
The program that asks servers for pages and draws them.
Request and response
The two halves of every visit: the ask, and the answer.
HTTP status 404
Not found: the server has nothing at that path.
Path
The part of a URL after the domain: /recipes/pancakes.
HTML, CSS, JavaScript
Content, appearance, behaviour.
Static page
HTML made before anyone visits; the same file for everyone.
Dynamic page
HTML made when a request arrives; can differ per visitor.
Island
A part of an Astro page that ships JavaScript so it can react.
Module 2: Tools, frameworks and the build step
Astro10 / 212
Module 2 · Tools, frameworks and the build step
The terminal is where you type commands
A terminal is a window where you type instructions for your computer instead of clicking. Web tools are mostly run from it. On a Mac it is called Terminal; on Windows, use PowerShell or Windows Terminal; on Linux, any terminal app.
Terminal
1pwd# print the folder you are in2ls# list what is in it3cdprojects# change into the "projects" folder4cd..# go back up one folder
Type a command, press Enter, read what it prints. Text after # is a comment, not part of the command.
Terminal
A text window for running commands. Also called a shell or command line.
Command
A program name, sometimes followed by words that tell it what to do: cd projects.
Folder (directory)
A place that holds files. The terminal is always "in" one folder at a time.
Astro11 / 212
Module 2 · Tools, frameworks and the build step
Node runs JavaScript on your computer, and npm installs tools
JavaScript was made for browsers, but Node.js (Node for short) runs it on your own computer too. Astro is a JavaScript program, so it needs Node. Node comes with npm, which downloads code other people have shared, called packages.
Terminal
1node--version# prints something like v22.x2npm--version# npm is installed with Node3npminstallastro# download the astro package
Node.js
A program that runs JavaScript outside a browser. Install it from nodejs.org.
npm
The Node package manager: it installs, updates and runs packages.
Package
Shared code with a name and a version, like astro or react.
Dependency
A package your project needs. They are listed in package.json.
Use a current Node. Astro 7 needs a recent Node 22 or newer. If a command fails with a message about the Node version, update Node first: it is the most common first error.
Astro12 / 212
Module 2 · Tools, frameworks and the build step
Check: which tool does what
QuizInteractive · disabled in the PDF
You want to add a package called date-fns to your project. What do you use?
You want to add a package called date-fns to your project. What do you use?
QuizInteractive · disabled in the PDF
You open a terminal and type npm run dev, but it says it can’t find package.json. What is the likely cause?
You open a terminal and type npm run dev, but it says it can’t find package.json. What is the likely cause?
Astro13 / 212
Module 2 · Tools, frameworks and the build step
A framework gives your project a shape
You could write a blog as plain .html files. Then every page repeats the same header and footer, and changing the menu means editing fifty files. A framework is a set of tools and rules that solves this kind of problem once: you write the header one time and reuse it everywhere.
Hand-written HTML
The header is copied into every file
A recipe list is written out by hand
Nothing checks your links or data
With a framework
The header is one component, reused
The list is made from your recipe files
Mistakes are caught before the site goes live
Framework
Tools plus conventions (where files go, how they are named) that do the repetitive work for you.
Component
A reusable piece of a page, like a header or a recipe card.
Astro14 / 212
Module 2 · Tools, frameworks and the build step
A build turns what you write into what the browser gets
You write in a form that is pleasant for people: components, Markdown, TypeScript. Browsers want plain HTML, CSS and JavaScript. The build is the step in between: a program reads your source files and writes the finished website, the output.
You edit source files on your computer.
The build reads them all and does the work once.
It writes the finished files into a folder called dist/.
You put that folder on a server, which sends the files to visitors.
Astro’s build: your source in, a folder of ready files out.
Source
The files you write and edit.
Build
Turning the source into the finished site, ahead of time.
Output
What the build writes: here, the dist/ folder.
Bundle
Many code files joined and shrunk into a few, so browsers download less.
You want to publish your blog to a hosting service. What do you give it?
You want to publish your blog to a hosting service. What do you give it?
Astro16 / 212
Module 2 · Tools, frameworks and the build step
Astro is built for sites that are mostly content
HTML first: pages are built to HTML, which browsers show fastest.
No JavaScript by default: a page sends none unless you add an island.
Content built in: Markdown files become pages, with their data checked.
Bring your favourite UI library: React, Vue, Svelte, Solid and others work as islands.
Static or per request: build pages ahead of time, or on a server when you must.
What sits underneath. Astro is built on Vite, a fast tool for serving and bundling web code. Astro 7 uses Vite 8. You rarely touch Vite directly, but its name shows up in error messages and in advanced settings.
Astro17 / 212
Module 2 · Tools, frameworks and the build step
The same page, two ways
Single-page app
The server sends an almost empty HTML page
A large JavaScript file then draws everything
Nothing shows until that script has run
Good for app-like tools you keep open
Astro page
The server sends the finished HTML
The page shows as soon as it arrives
Only islands download JavaScript
Good for blogs, docs, shops, portfolios
Single-page app (SPA)
A site where JavaScript in the browser draws every page, instead of the server sending finished HTML.
QuizInteractive · disabled in the PDF
A reader on a slow phone opens a recipe. With which approach do they see the text sooner?
A reader on a slow phone opens a recipe. With which approach do they see the text sooner?
Astro18 / 212
Module 2 · Tools, frameworks and the build step
Recap: tools, frameworks and the build
The terminal runs commands; cd moves between folders.
Node runs JavaScript on your computer; npm installs packages listed in package.json.
A framework solves repetitive work once: components, conventions, checks.
The build turns your source into finished files in dist/.
The dev server is for writing; the build is for publishing.
Astro sends HTML first and JavaScript only where you ask for it.
FlashcardsInteractive · disabled in the PDF
Terminal
A text window where you type commands.
cd
Change directory: move the terminal into another folder.
Node.js
Runs JavaScript outside the browser; Astro needs it.
npm
Installs and runs packages.
package.json
The project’s list of dependencies and scripts.
Framework
Tools and conventions that do repetitive work for you.
Build
Turning source files into the finished site, ahead of time.
dist/
The folder Astro’s build writes: what visitors get.
Vite
The tool under Astro that serves and bundles code.
Single-page app
JavaScript draws every page in the browser.
Try itInteractive · disabled in the PDF
Module 3: Your first Astro project
Astro19 / 212
Module 3 · Your first Astro project
One command makes a new project
Terminal
1cd~/projects2npmcreateastro@latest
npm create downloads Astro’s setup tool and runs it. @latest asks for its newest version.
The setup tool asks a few questions in the terminal: where to create the project (a folder name, like kitchen-notes), which starting template to use (choose the empty or minimal one, so you see every file you add), whether to install dependencies (yes), and whether to start a git repository (your choice).
Terminal
1cdkitchen-notes2npmrundev
Move into the new folder and start the dev server.
Template
A ready-made starting project that the setup tool copies for you.
Git repository
A folder whose history of changes is tracked by git, a version control tool. Optional for now.
Astro20 / 212
Module 3 · Your first Astro project
Each folder has one job
kitchen-notes/
public/# copied to the site as is
favicon.svg
src/# everything Astro processes
pages/# one file per page of the site
index.astro# the home page: /
astro.config.mjs# Astro's settings
package.json# dependencies and scripts
tsconfig.json# TypeScript settings
node_modules/# installed packages (don't edit)
A fresh minimal project. You will add src/components/ and src/layouts/ soon.
src/pages/ is special: every file in it becomes a page with its own address.
public/ is for files that must keep their exact name and content, like favicon.svg or robots.txt.
node_modules/ is filled by npm. It is large and never edited by hand; you can delete it and run npm install to get it back.
QuizInteractive · disabled in the PDF
You want a new page at /contact. Where do you create it?
You want a new page at /contact. Where do you create it?
npm run dev runs whatever is written next to "dev": here, astro dev. The ^ in "^7.4.0" means "7.4.0 or any newer 7.x", so a fresh install can get a later bug-fix release but never Astro 8.
Fill the blanksInteractive · disabled in the PDF
Fill in the commands: start writing, then make the finished site, then look at it.
Terminal
1npmrun# live while you write2npmrun# write dist/3npmrun# serve dist/ locally
Astro22 / 212
Module 3 · Your first Astro project
The dev server shows your site at localhost
Text
1astro v7.4.10 ready in 312 ms23┃ Local http://localhost:4321/4┃ Network use --host to expose
What npm run dev prints (the details vary). Open the Local address in your browser.
localhost
A name that always means "this computer". Only you can open it.
Port
A number that picks one program on a computer. Astro’s dev server uses 4321 by default.
Hot reload
The browser updating by itself a moment after you save a file.
Stopping it. The dev server keeps running until you stop it. Press Ctrl and C in the terminal. If port 4321 is busy, Astro picks the next free one and prints it.
Astro23 / 212
Module 3 · Your first Astro project
Your home page is one file
WalkthroughInteractive · disabled in the PDF
src/pages/index.astro
1---2const name ='Sam'3---45<htmllang="en">6<head>7<metacharset="utf-8"/>8<metaname="viewport"content="width=device-width"/>9<title>Kitchen notes</title>10</head>11<body>12<h1>{name}’s kitchen notes</h1>13<p>Recipes and stories from a small kitchen.</p>14</body>15</html>
Lines 1-3: Between the two --- lines is JavaScript (or TypeScript) that runs while the page is built. Here it makes a variable.
Lines 5: Below, ordinary HTML. lang="en" tells browsers and screen readers the language.
Lines 7-8:charset makes accented letters show correctly; viewport makes the page fit phone screens.
Lines 9: The title shown on the browser tab and in search results.
Lines 12:{name} puts the variable’s value into the HTML: the page says "Sam’s kitchen notes".
Astro24 / 212
Module 3 · Your first Astro project
A new file in src/pages is a new address
src/pages/about.astro
1<htmllang="en">2<head>3<metacharset="utf-8"/>4<title>About</title>5</head>6<body>7<h1>About me</h1>8<p>I cook on weekends and write it down.</p>9<ahref="/">Back home</a>10</body>11</html>
No frontmatter needed when there is nothing to compute. The page is at /about.
Save it and open http://localhost:4321/about. A link between your pages is a plain <a href> with the path. Repeating the <head> in every page is tedious: module 9 replaces it with a layout.
QuizInteractive · disabled in the PDF
You create src/pages/recipes.astro. What address shows it?
You create src/pages/recipes.astro. What address shows it?
Astro25 / 212
Module 3 · Your first Astro project
Build writes the finished site into dist
Terminal
1npmrunbuild# writes dist/2npmrunpreview# serves dist/ at localhost:4321
dist/
index.html# from src/pages/index.astro
about/
index.html# from src/pages/about.astro
favicon.svg# copied from public/
Each page becomes a folder with an index.html, so /about works on any host.
Open dist/about/index.html in your editor: it is plain HTML, with the frontmatter gone and every {expression} replaced by its value. That folder is your whole website.
QuizInteractive · disabled in the PDF
You fix a typo directly in dist/index.html, then run npm run build again. What happens to the fix?
You fix a typo directly in dist/index.html, then run npm run build again. What happens to the fix?
Astro26 / 212
Module 3 · Your first Astro project
Read an error from the top: what, where, which line
When something is wrong, the dev server shows an error page in the browser and prints the same error in the terminal. Read it calmly: the first line says what went wrong, then comes the file and the line, often with the code around it.
Text
1ReferenceError: nme is not defined2 src/pages/index.astro:12:83 11 | <body>4> 12 | <h1>{nme}’s kitchen notes</h1>5 | ^
An illustrative error: the variable is called name, but line 12 says nme.
Find the bugInteractive · disabled in the PDF
This page fails to build with a syntax error. Which line is wrong?
defineConfig changes nothing by itself; it lets your editor check and complete the options.
site is the public address your site will have. Astro uses it to build full links (for search engines and feeds). Later modules add more here: integrations like React, an adapter for a server, image and i18n settings. After changing the config, the dev server restarts itself.
Try itInteractive · disabled in the PDF
Astro28 / 212
Module 3 · Your first Astro project
Recap: your first project
npm create astro@latest makes a project; npm run dev runs it at localhost:4321.
Files in src/pages/ become pages: about.astro is /about, index.astro is /.
public/ is copied as is; node_modules/ is npm’s; dist/ is the build’s.
npm run build writes dist/; npm run preview serves it so you can check it.
Errors say what, then where: read the first line, then the file and line.
astro.config.mjs holds settings, starting with site.
FlashcardsInteractive · disabled in the PDF
npm create astro@latest
Creates a new Astro project with a setup tool.
src/pages/
One file per page; the file’s path becomes the address.
public/
Files copied to the site unchanged, like favicon.svg.
localhost:4321
Your own computer, port 4321: where the dev server runs.
npm run preview
Serves the built dist/ folder locally.
Frontmatter
The code between the --- lines at the top of an .astro file.
astro.config.mjs
The project’s Astro settings, made with defineConfig.
site
The config option holding the site’s public address.
Module 4: Inside an .astro file
Astro29 / 212
Module 4 · Inside an .astro file
An .astro file has a script on top and a template below
src/pages/recipes.astro
1---2const title ='Recipes'3const count =124---56<h1>{title}</h1>7<p>{count} recipes so far.</p>
Code fence
The two --- lines. Between them is the component script, written in JavaScript or TypeScript.
Frontmatter
Another name for the component script, borrowed from Markdown files, which put data at the top the same way.
Template
Everything after the fence: HTML, plus { } wherever you want a value from the script.
TypeScript in a sentence. TypeScript is JavaScript with labels saying what kind of value each thing is (string, number…), so mistakes show up in your editor before you run anything. Astro understands it with no setup.
Astro30 / 212
Module 4 · Inside an .astro file
The script runs while the page is built, never in the browser
For a static page, the script runs once, during the build (and on each refresh under astro dev). Its result is HTML. None of the script is sent to the browser: visitors can’t see it, and it can’t react to clicks.
Astro
1---2console.log('Building the recipes page')3const builtAt =newDate().toLocaleDateString('en-GB')4---56<p>Page built on {builtAt}.</p>
QuizInteractive · disabled in the PDF
Where does the console.log message appear?
Where does the console.log message appear?
Astro31 / 212
Module 4 · Inside an .astro file
Curly braces put values into the HTML
Astro
1---2const recipe ={name:'Lentil soup',minutes:40}3const photo ='/images/soup.jpg'4---56<h2>{recipe.name}</h2>7<p>Ready in {recipe.minutes} minutes.</p>8<p>With resting time: {recipe.minutes +10} minutes.</p>9<imgsrc={photo}alt="A bowl of lentil soup"/>
Inside { } goes any JavaScript expression: something that has a value, like recipe.name or 2 * 3.
For an attribute, put the braces in place of the quotes: src={photo}.
Text in braces is escaped: a < in your data shows as a < character, it can’t become a tag.
Fill the blanksInteractive · disabled in the PDF
Show the recipe’s name in the heading, and use its photo as the image source.
Astro
1---2const recipe ={name:'Flatbread',photo:'/img/bread.jpg'}3---4<h2></h2>5<imgsrc=alt="Flatbread on a board"/>
1---2const ingredients =['2 eggs','200 g flour','300 ml milk']3---45<ul>6{ingredients.map((item)=><li>{item}</li>)}7</ul>
map calls the arrow function once per item and collects what it returns.
Array
An ordered list of values in JavaScript: ['a', 'b'].
`map`
An array method that makes a new array by running a function on each item.
Arrow function
A short way to write a function: (item) => <li>{item}</li> takes item and returns the <li>.
Find the bugInteractive · disabled in the PDF
The page shows an empty list. Which line is wrong?
Astro
Astro34 / 212
Module 4 · Inside an .astro file
Attributes: plain, computed, shorthand and spread
Astro
1---2const href ='/recipes/pancakes'3const label ='Pancakes'4const extra ={title:'Open the recipe',target:'_self'}5const isNew =true6---78<ahref="/about">About</a>9<ahref={href}>{label}</a>10<a {href}>{label}</a>11<ahref={href} {...extra}>{label}</a>12<buttondisabled={!isNew}>New this week</button>
Line 8: a fixed value, in quotes, as in HTML.
Line 9: a value from the script, in braces.
Line 10: shorthand: {href} means href={href} when the names match.
Line 11: spread: {...extra} adds every key of the object as an attribute.
Line 12: false leaves a boolean attribute out; true writes it.
It is `class`, not `className`. Astro templates use HTML’s own attribute names: class, for, tabindex. If you know React, that is the difference to remember.
A made-up address: any public JSON file works the same way.
fetch asks a server for something; await waits for the answer before going on. You can use await directly in the script. On a static page the fetch happens during the build: the tips are written into the HTML, and visitors never call that address.
QuizInteractive · disabled in the PDF
The tips file changes after you build the site. What do visitors see?
The tips file changes after you build the site. What do visitors see?
Astro36 / 212
Module 4 · Inside an .astro file
Fragments, raw HTML and comments
Astro
1---2const intro ='<em>Simple</em> food, done well.'3---45<!-- An HTML comment: sent to the browser -->6{/* A script comment: removed from the output */}7<Fragmentset:html={intro}/>8<>9<dt>Serves</dt>10<dd>4</dd>11</>
<!-- --> stays in the page’s source, where anyone can read it. {/* */} disappears.
<Fragment> (or <>…</>) groups elements without adding a wrapper element.
set:html inserts a string as HTML, without escaping it.
set:html trusts its input. With set:html, a string containing <script> becomes a real script. Use it only for HTML you wrote or have cleaned, never for text from visitors (a comment, a name). Module 18 shows why.
Astro37 / 212
Module 4 · Inside an .astro file
Recap: inside an .astro file
The script between --- runs while the page is made; the template below becomes HTML.
None of the script reaches the browser, and it runs at build time for static pages.
{expression} puts a value in text or an attribute, escaped.
&& and ? : show things conditionally; .map() turns arrays into elements.
Attributes can be computed, shorthand {href}, or spread {...attrs}.
await works in the script; set:html inserts raw HTML, so only trusted HTML.
FlashcardsInteractive · disabled in the PDF
Code fence
The --- lines around the component script.
When does the script of a static page run?
Once, during the build.
Expression
A piece of JavaScript that has a value: recipe.name, 2 * 3.
Escaping
Turning < and friends into plain characters so data can’t become HTML.
cond && <p/>
Renders the element only when cond is true.
Ternary
a ? x : y: x when a is true, else y.
<Fragment>
Groups elements without a wrapper; also written <>…</>.
set:html
Inserts a string as unescaped HTML. Trusted content only.
Try itInteractive · disabled in the PDF
Module 5: Components and props
Astro38 / 212
Module 5 · Components and props
A component is a piece of page you write once
Your blog’s home page and recipe page both show the same header. Instead of copying it, you put it in its own file, a component, and use it like an HTML tag. Change the file, and every page that uses it changes.
index.astrothe page
Headersite name and menu
main
RecipeCard× 3
Footer
A page built from components.
Components live in src/components/. Unlike src/pages/, files there never become pages by themselves.
Names start with a capital letter (Header.astro), so Astro can tell them apart from HTML tags.
An .astro file is a component. A page is just a component that lives in src/pages/.
Module 10 replaces this hand-written array with real recipe files.
QuizInteractive · disabled in the PDF
A fourth recipe is added to the array. What else must change for it to appear?
A fourth recipe is added to the array. What else must change for it to appear?
Astro43 / 212
Module 5 · Components and props
Astro components render once, to HTML
An Astro component runs on the server, produces HTML, and is gone. It has no memory between visits and no way to respond to a click by itself. That is what makes it free for the browser: there is nothing to download but HTML.
Predict the outputInteractive · disabled in the PDF
What happens with this Astro component when a reader clicks the button?
Astro
1---2let likes =03---4<buttononclick={()=> likes++}>5 Like ({likes})6</button>
What happens with this Astro component when a reader clicks the button?
Props flow down. A page passes props to a component; the component can’t change them or send anything back. Data always flows from parent to child, which makes a page easy to follow: to know what a component shows, look at what it was given.
HTMLAttributes<'a'> is every attribute an <a> accepts. ...rest collects the props you didn’t take out by name, and {...rest} puts them on the element. Now <ButtonLink href="/about" title="About Sam" /> works, and a typo like hraf is flagged.
Find the bugInteractive · disabled in the PDF
Callers can’t add their own aria-label: it never appears. Which line is wrong?
Astro
Astro45 / 212
Module 5 · Components and props
Small components, clear names
src/
components/
Header.astro
Footer.astro
RecipeCard.astro
ButtonLink.astro
TagList.astro
pages/
index.astro
about.astro
recipes.astro
Make a component when something repeats, or when a page gets long enough to be hard to read.
Name it after what it is (RecipeCard), not where it is used (HomePageThing).
Give it only the props it needs: name and href, not the whole site’s data.
Try itInteractive · disabled in the PDF
Astro46 / 212
Module 5 · Components and props
Recap: components and props
A component is an .astro file in src/components/, named with a capital letter.
Import it in the script and use it like a tag: <Header />.
Props are passed like attributes and read from Astro.props.
interface Props types them; ? makes one optional, = value gives a default.
Astro components render once to HTML: no state, no click handlers.
HTMLAttributes and ...rest pass through attributes you don’t name.
FlashcardsInteractive · disabled in the PDF
Component
A reusable piece of page, used like a tag.
Astro.props
The props a component received, as an object.
interface Props
The TypeScript description of a component’s props.
Optional prop
Marked with ? in the interface; often given a default.
Destructuring
const { a, b } = obj: take values out of an object by name.
...rest
Collects the remaining props; {...rest} spreads them onto an element.
astro check
Checks types in .astro files from the terminal.
Do Astro components run in the browser?
No. They render to HTML and are gone.
Module 6: Slots: components that wrap other things
Astro47 / 212
Module 6 · Slots: components that wrap other things
Props carry values; slots carry markup
Props are good for small values: a title, a number, a link. But some components exist to wrap things: a card around a recipe, a box around a warning, a page frame around everything. What goes inside changes every time, and it is markup, not a value. Squeezing a whole paragraph with links and pictures into a prop would be awkward.
A slot is a hole a component leaves in its template. The parent writes markup between the component’s opening and closing tags, and Astro puts that markup where the hole is.
A prop
A value: text, number, list, object
Read in the frontmatter with Astro.props
Written as an attribute
A slot
Markup: elements, other components, text
Placed with <slot /> in the template
Written between the tags
Slot
A place in a component’s template where markup from the parent goes.
Children
The markup a parent writes between a component’s opening and closing tags.
Astro48 / 212
Module 6 · Slots: components that wrap other things
1---2import Card from'../components/Card.astro'3---4<Card>5<h2>Lemon drizzle cake</h2>6<p>Sharp, sweet and ready in an hour.</p>7</Card>
The <h2> and <p> replace <slot />. The card decides the frame (the <article>, the <div>), the page decides what goes in it. The children are rendered by the parent, so they can use the parent’s variables.
Astro49 / 212
Module 6 · Slots: components that wrap other things
Check: where do the children go
Predict the outputInteractive · disabled in the PDF
What HTML does this produce?
Astro
1<!-- Note.astro -->2<asideclass="note"><strong>Note:</strong><slot/></aside>34<!-- in a page -->5<Note>Use cold butter.</Note>
What HTML does this produce?
No slot, no children. If a component has no <slot />, anything you write between its tags is silently dropped. When content goes missing, check the component has a slot for it.
Astro50 / 212
Module 6 · Slots: components that wrap other things
Named slots fill more than one hole
A card often has a header, a body and a footer. Give each hole a name with <slot name="…" />. The parent then says where each piece goes with a slot="…" attribute. Anything without a slot attribute goes to the default (unnamed) slot.
1<Card>2<h2slot="header">Lemon drizzle cake</h2>3<p>Sharp, sweet and ready in an hour.</p>4<aslot="footer"href="/recipes/lemon-drizzle/">Read it</a>5</Card>
The order in the parent doesn’t matter: the component’s template decides.
Astro51 / 212
Module 6 · Slots: components that wrap other things
Practise: fill a named slot
Fill the blanksInteractive · disabled in the PDF
The layout has a sidebar slot. Put the list of tags in it, and the post in the default slot.
Astro
1<!-- TwoColumns.astro -->2<main><slot/></main>3<aside><slot="sidebar"/></aside>45<!-- in a page -->6<TwoColumns>7<ul="sidebar">8<li>baking</li>9<li>quick</li>10</ul>11<article>Today I baked bread.</article>12</TwoColumns>
Astro52 / 212
Module 6 · Slots: components that wrap other things
Fallback content shows when a slot is empty
Anything you put inside a <slot> element is its fallback: shown only when the parent passes nothing for that slot. It is a default value, but for markup.
src/components/Card.astro (footer)
1<footer>2<slotname="footer">3<spanclass="muted">No link yet</span>4</slot>5</footer>
Parent passes slot="footer" content: that content shows, the fallback doesn’t.
Parent passes nothing for footer: “No link yet” shows.
Astro53 / 212
Module 6 · Slots: components that wrap other things
Astro.slots.has asks if a slot was filled
Sometimes an empty slot should remove its wrapper too. An empty <footer> still takes space and borders. In the frontmatter, Astro.slots.has("footer") is true only if the parent passed something for that slot. The default slot is called "default".
The footer never shows, even when the page passes one. Which line is wrong?
Astro
Astro54 / 212
Module 6 · Slots: components that wrap other things
Several elements in one slot, and passing slots along
A slot attribute goes on one element. To put several elements in the same named slot without adding a wrapper element, use <Fragment slot="…">: a fragment groups things and leaves no tag of its own.
Astro
1<Card>2<Fragmentslot="footer">3<ahref="/recipes/lemon-drizzle/">Read it</a>4<span>45 minutes</span>5</Fragment>6<p>Sharp, sweet and ready in an hour.</p>7</Card>
A component that wraps another can hand a slot straight through: <slot name="footer" slot="footer" /> takes what its parent passed for footer and gives it to the inner component’s footer.
Astro.slots.render("name") renders a slot to a string of HTML in the frontmatter, for when you need to inspect or transform it. If a parent passes a function as a slot’s child, Astro.slots.render("default", [arg]) calls it with arguments, which lets a component hand data back to its parent (a “render prop”). Both are rare: plain <slot /> covers almost everything.
Astro55 / 212
Module 6 · Slots: components that wrap other things
Check: props or a slot
QuizInteractive · disabled in the PDF
Which of these are better as a slot than as a prop?
Which of these are better as a slot than as a prop?
Slots are rendered lazily. A slot’s content is only rendered if the component outputs that slot. Content passed to a slot the component never places (or hides behind a false condition) isn’t rendered at all, so components inside it don’t run. That makes conditional slots cheap, and it is why Astro.slots.has checks whether content was passed, not whether it renders to something non-empty.
Astro56 / 212
Module 6 · Slots: components that wrap other things
Recap: slots
A slot is a hole in a component’s template; the parent’s children fill it.
<slot /> is the default slot: everything without a slot attribute goes there.
<slot name="x" /> declares a named slot; slot="x" on a child fills it.
Markup inside <slot> is the fallback, shown when nothing is passed.
Astro.slots.has("x") lets you drop a wrapper when a slot is empty.
<Fragment slot="x"> puts several elements in one slot; <slot name="x" slot="x" /> passes one along.
Values go in props, markup goes in slots.
FlashcardsInteractive · disabled in the PDF
Slot
A place in a component’s template where the parent’s markup goes.
<slot />
The default slot: receives children with no slot attribute.
<slot name="footer" />
Declares a named slot called footer.
slot="footer"
On a child: sends it to the footer slot.
Fallback content
Markup inside a <slot>, shown only when nothing is passed for it.
Astro.slots.has(name)
true if the parent passed content for that slot.
<Fragment slot="x">
Groups several elements into one slot without adding a tag.
Try itInteractive · disabled in the PDF
Module 7: Styles and small scripts
Astro57 / 212
Module 7 · Styles and small scripts
CSS says how things look
HTML says what is on the page; CSS (Cascading Style Sheets) says how it looks. A CSS rule is a selector (which elements) and a block of declarations (property and value pairs).
Lines 2-3: Two declarations: text colour, and a size twice the root font size (rem).
Lines 6:.card selects every element with class="card". The dot means “class”.
Lines 7-8: Space inside the box, and a thin grey line around it.
Selector
The part of a rule that picks elements: h1, .card, nav a.
Class
A name given to elements with class="…", so CSS can select them.
Cascade
How the browser decides between rules that disagree: more specific selectors and later rules win.
Astro58 / 212
Module 7 · Styles and small scripts
A <style> in a component only styles that component
src/components/RecipeCard.astro
1---2const{ title }= Astro.props3---4<articleclass="card">5<h2>{title}</h2>6</article>78<style>9.card{padding:1rem;border-radius:8px;}10h2{color:darkgreen;}11</style>
Styles in an .astro component are scoped: Astro adds a unique marker to this component’s elements and to its selectors, so h2 here only matches the <h2> inside this card, never the page title. You can use short, plain selectors without fear of breaking the rest of the site.
Roughly what the build produces: the marker ties the rule to this component.
Going deeper
The marker is an attribute by default (scopedStyleStrategy: "attribute" in astro.config.mjs). It can also be a class ("class") or a :where() selector ("where"), which changes how specific the scoped selectors are. Astro bundles the styles into CSS files, and inlines small ones (build.inlineStylesheets).
Astro59 / 212
Module 7 · Styles and small scripts
Check: what a scoped rule reaches
QuizInteractive · disabled in the PDF
RecipeCard.astro has <style> p { color: gray; } </style>. The page around it also has paragraphs. Which ones turn gray?
RecipeCard.astro has <style> p { color: gray; } </style>. The page around it also has paragraphs. Which ones turn gray?
Slotted content and child components. Markup passed in through a slot belongs to the parent that wrote it, and the inside of a child component belongs to the child: a component’s scoped rules reach neither. Style them where they are written, or reach in on purpose with :global() (next slide).
Astro60 / 212
Module 7 · Styles and small scripts
Global styles: a stylesheet, is:global or :global()
Way
What it does
Use it for
import "../styles/global.css"
Adds a normal, unscoped stylesheet to the page
Fonts, colours, resets for the whole site
<style is:global>
The whole block is unscoped
Rare: prefer a stylesheet
.post :global(a)
Only the part inside :global() is unscoped
Markup you didn’t write, like rendered Markdown
src/components/Prose.astro
1<divclass="prose"><slot/></div>23<style>4/* links inside whatever was slotted in */5.prose :global(a) {text-decoration:underline;}6</style>
.prose stays scoped to this component; the a inside it can be anyone’s.
Astro61 / 212
Module 7 · Styles and small scripts
class:list builds a class from conditions
To add a class only sometimes, class:list takes a list of strings, objects and arrays, keeps what is truthy and joins it into one class attribute.
src/components/TagLink.astro
1---2interfaceProps{ tag:string; active?:boolean}3const{ tag, active =false}= Astro.props4---5<a6href={`/tags/${tag}/`}7class:list={['tag',{ active }]}8>9{tag}10</a>
With active true: class="tag active". Otherwise: class="tag".
Fill the blanksInteractive · disabled in the PDF
Give the recipe card the class quick only when it takes under 30 minutes.
Astro
1<article={['card',{quick: minutes 30}]}>
Astro62 / 212
Module 7 · Styles and small scripts
define:vars passes values into CSS
A CSS custom property (a variable, written --name) can carry a value from the frontmatter into a <style>. define:vars sets them for you, on this component’s elements.
src/components/TagBadge.astro
1---2const{ color ='tomato'}= Astro.props3---4<spanclass="badge"><slot/></span>56<styledefine:vars={{ color }}>7.badge{background:var(--color);}8</style>
Values, not secrets. The values end up written in the HTML as inline styles, and they are fixed when the page is rendered. Use define:vars for colours and sizes chosen on the server, not for anything that changes in the browser.
Astro63 / 212
Module 7 · Styles and small scripts
Tailwind: styles as classes
Tailwind CSS is a library of small classes, each doing one thing (p-4 is padding, text-green-800 a colour). You style by listing classes in the markup instead of writing rules. npx astro add tailwind installs it as a Vite plugin and creates a stylesheet with @import "tailwindcss"; for you to import in your layout.
Both are fine, and they mix: many sites use Tailwind for most things and a scoped <style> for the odd case. Pick one default so the codebase reads the same everywhere.
Astro64 / 212
Module 7 · Styles and small scripts
A <script> adds a little behaviour
A page without JavaScript can’t react to a click. For small behaviour, a <script> in a component is enough. Astro processes it: TypeScript works, import works, it is bundled and minified (into a file, or straight into the page when it is small and imports nothing), and it is included once per page however many times the component appears.
Because it runs once, the script finds every button with querySelectorAll, not just one. It runs in the browser, after the HTML is there, so it can’t read the frontmatter’s variables.
Going deeper
Another clean pattern is a custom element: class CopyLink extends HTMLElement { connectedCallback() { … } } and customElements.define("copy-link", CopyLink). The browser runs connectedCallback for each <copy-link> element, so each copy sets itself up. To pass a value from the server, put it in a data-* attribute and read this.dataset.
Astro65 / 212
Module 7 · Styles and small scripts
is:inline leaves a script exactly as written
`<script>` (processed)
TypeScript and imports work
Bundled and minified; a big one becomes a cached file
Included once per page
Runs after the page is read (a module)
`<script is:inline>`
Plain JavaScript only, no imports from your project
Written into the HTML as is
Repeated each time the component is used
Runs right where it is
Use is:inline only when the script must run before the page paints, such as applying a saved dark theme to <html> so the page doesn’t flash light first. define:vars on a script also makes it inline.
Find the bugInteractive · disabled in the PDF
The page shows three CopyLink buttons, but only the first one works. Which line is the bug?
Astro
Astro66 / 212
Module 7 · Styles and small scripts
Recap: styles and scripts
A CSS rule is a selector plus declarations; the cascade settles conflicts.
A component’s <style> is scoped: it only reaches the elements that component writes.
Site-wide styles go in an imported stylesheet; :global() reaches markup you didn’t write.
class:list adds classes from conditions; define:vars passes values into CSS.
Tailwind styles with small classes, and mixes with scoped styles.
A <script> is bundled and runs once per page; is:inline is left as written.
FlashcardsInteractive · disabled in the PDF
CSS
Cascading Style Sheets: rules that say how HTML looks.
Scoped style
A component’s <style>, limited by Astro to that component’s elements.
:global(…)
Inside a scoped style, the part in brackets is not scoped.
class:list
Joins strings, objects and arrays into a class, keeping the truthy parts.
define:vars
Passes frontmatter values into a <style> (or script) as CSS variables.
Tailwind CSS
A library of single-purpose classes, added with npx astro add tailwind.
Processed <script>
Bundled by Astro, TypeScript allowed, included once per page.
is:inline
Leaves a <script> or <style> untouched, right where it is written.
Try itInteractive · disabled in the PDF
Module 8: Pages, links and routes
Astro67 / 212
Module 8 · Pages, links and routes
Each file in src/pages/ is an address
A route is an address on your site and the code that answers it. Astro uses file-based routing: you don’t write a list of routes, the files in src/pages/are the list. The path of the file becomes the path of the address.
File
Address
src/pages/index.astro
/
src/pages/about.astro
/about
src/pages/recipes/index.astro
/recipes
src/pages/recipes/soup.astro
/recipes/soup
src/pages/404.astro
Shown for any address that doesn’t exist
Route
An address and the file that answers it.
File-based routing
Routes come from the files in src/pages/, not from a list in code.
Index file
index.astro answers for its folder itself: /recipes for recipes/index.astro.
Astro68 / 212
Module 8 · Pages, links and routes
Links are plain <a> tags
There is no special link component. You link with the HTML <a href="…"> element, and following it loads the other page from the server, the way the web has always worked. Start addresses with / so they don’t depend on the page you are on.
Trailing slashes. Whether /about or /about/ is the “real” address depends on your host and on two settings: trailingSlash ("ignore" by default, or "always" / "never") and build.format, which decides if about.astro is built as about/index.html (the default, "directory") or about.html. Pick one style and write every link that way.
QuizInteractive · disabled in the PDF
You want a page at /recipes/breakfast. Which files would answer it?
You want a page at /recipes/breakfast. Which files would answer it?
Astro69 / 212
Module 8 · Pages, links and routes
Brackets in a file name make a dynamic route
You don’t want one file per recipe. A dynamic route is one file for many addresses: name it with a parameter in square brackets, [slug].astro, and the matching part of the address becomes Astro.params.slug. A slug is the readable, URL-safe name of something: lemon-drizzle for “Lemon drizzle cake”.
Astro builds pages ahead of time (module 1), so it has to know which slugs exist before any visitor arrives. A dynamic page tells it by exporting a function called getStaticPaths (next slide).
Dynamic route
One file that answers many addresses, using a [parameter] in its name.
Parameter
The changing part of the address, read as Astro.params.name.
Slug
A short, URL-safe name for an item, like lemon-drizzle.
Lines 2: A plain list of recipes from a TypeScript file. In module 10 it comes from Markdown files instead.
Lines 4:getStaticPaths is exported, and Astro calls it once during the build.
Lines 5-8: It returns one object per page: params fills the brackets in the file name, props is handed to that page.
Lines 11: For each page, the template runs with that recipe as Astro.props.
Lines 13-14: The same template, a different recipe on every page.
Pass the data as props. You could look the recipe up again from Astro.params.slug, but you already had it in getStaticPaths. Passing it as props avoids a second search and can’t get out of step.
Astro71 / 212
Module 8 · Pages, links and routes
Practise: tags pages
Fill the blanksInteractive · disabled in the PDF
Build one page per tag at /tags/[tag], each with the recipes that have that tag.
Astro
1---2// src/pages/tags/[tag].astro3import{ recipes }from'../../data/recipes'45exportfunction(){6const tags =newSet(recipes.flatMap((r)=> r.tags))7return[...tags].map((tag)=>({8:{ tag },9props:{10list: recipes.filter((r)=> r.tags.includes(tag)),11},12}))13}14const{ tag }= Astro.15const{ list }= Astro.16---17<h1>Tagged “{tag}”</h1>
Astro72 / 212
Module 8 · Pages, links and routes
A rest parameter matches several levels
[slug] matches one piece of the address, between two slashes. [...path] is a rest parameter: it matches any number of pieces, including none. Its value is the whole matched part, as one string with slashes.
File
Address
Astro.params
docs/[...path].astro
/docs/kitchen/knives
{ path: "kitchen/knives" }
docs/[...path].astro
/docs
{ path: undefined }
[lang]/[slug].astro
/es/sopa
{ lang: "es", slug: "sopa" }
In getStaticPaths, give a rest parameter undefined to build the top-level address itself (/docs).
Astro73 / 212
Module 8 · Pages, links and routes
When two routes match, the more specific one wins
Several files can match the same address. Astro picks by route priority: fixed names beat parameters, a named parameter beats a rest parameter, and more levels beat fewer.
Put in orderInteractive · disabled in the PDF
Put these files in the order Astro tries them for /recipes/new, first to last.
1.src/pages/recipes/new.astro
2.src/pages/recipes/[...path].astro
3.src/pages/[...all].astro
4.src/pages/recipes/[slug].astro
Astro74 / 212
Module 8 · Pages, links and routes
paginate() splits a long list into pages
Pagination splits a long list over numbered pages: ten recipes on /recipes/1, the next ten on /recipes/2. getStaticPaths receives a paginate helper that makes those pages for you.
page.data holds this page’s items. page.url.prev and page.url.next are undefined at the ends, so the links only show when there is somewhere to go. Name the file [...page].astro instead and the first page is /recipes rather than /recipes/1.
Astro75 / 212
Module 8 · Pages, links and routes
Not found pages and redirects
src/pages/404.astro is the page for addresses that don’t exist. Build it like any page, with a link back home. When a page moves, a redirect sends visitors from the old address to the new one, so old links keep working.
Redirects can use the same [param] names on both sides.
Find the bugInteractive · disabled in the PDF
The build fails with a missing parameter error for src/pages/blog/[slug].astro. Which line is wrong?
Astro
Going deeper
In a prerendered site, a redirect is built as a small HTML page that sends the browser on with a <meta http-equiv="refresh">. On a server (module 13), a route can also answer with return Astro.redirect("/recipes/", 301). 301 means “moved for good”, 302 “for now”.
Astro76 / 212
Module 8 · Pages, links and routes
Recap: routing
Files in src/pages/ are routes; their path is the address; index.astro answers for its folder.
Link with plain <a href="/…">, in one trailing slash style.
[slug].astro is a dynamic route; getStaticPaths returns params and props per page.
[...path] matches any number of levels, including none.
Fixed names beat [param], which beats [...rest].
paginate() makes numbered pages with page.data and page.url.
404.astro handles missing addresses; redirects keeps old links alive.
FlashcardsInteractive · disabled in the PDF
Route
An address and the file in src/pages/ that answers it.
Dynamic route
A file with [param] in its name, answering many addresses.
getStaticPaths
Exported by a dynamic page: returns the params and props of every page to build.
Astro.params
The parameters taken from the address, like { slug: "soup" }.
Rest parameter
[...path]: matches any number of address pieces, or none.
Route priority
Fixed names, then named parameters, then rest parameters.
paginate()
A helper in getStaticPaths that splits a list into numbered pages.
redirects
A config option mapping old addresses to new ones.
Try itInteractive · disabled in the PDF
Module 9: Layouts and the page head
Astro77 / 212
Module 9 · Layouts and the page head
Every page repeats the same frame
Each page needs <html>, a <head> with a title and character set, a header with the navigation and a footer. Copying that into every page means changing ten files to fix one link. A layout is a component that holds that frame once, with a <slot /> where each page’s own content goes.
<html> and <head>title, description, styles
Headersite name and navigation
<slot />the page’s own content
Footer
The layout is the frame; each page fills the middle.
Layout
A component that renders the shared frame of pages, with a slot for each page’s content.
`<head>`
The part of an HTML page that isn’t shown: title, description, links to styles and icons.
Astro78 / 212
Module 9 · Layouts and the page head
A base layout, line by line
WalkthroughInteractive · disabled in the PDF
src/layouts/BaseLayout.astro
1---2import Header from'../components/Header.astro'3import'../styles/global.css'45interfaceProps{ title:string; description?:string}6const{ title, description ='Recipes and notes'}= Astro.props7---8<!doctypehtml>9<htmllang="en">10<head>11<metacharset="utf-8"/>12<metaname="viewport"content="width=device-width"/>13<title>{title} · Sam’s kitchen notes</title>14<metaname="description"content={description}/>15</head>16<body>17<Header/>18<main><slot/></main>19<footer>Written by Sam</footer>20</body>21</html>
Lines 2-3: The header component and the site-wide stylesheet, imported once here for every page.
Lines 5-6: Each page passes its own title, and may pass a description.
Lines 8-9: The doctype and lang, so browsers and screen readers know the page is HTML in English.
Lines 11-12: The character set, and a viewport that makes phones show the page at their own width.
Lines 13-14: The title in the browser tab and in search results, and the summary shown under it.
Lines 17-19: The frame around the page, with the page’s content in the slot.
Astro79 / 212
Module 9 · Layouts and the page head
A page wraps itself in the layout
src/pages/about.astro
1---2import BaseLayout from'../layouts/BaseLayout.astro'3---4<BaseLayouttitle="About"description="Who writes this blog">5<h1>About me</h1>6<p>I cook on weekdays and write about it on weekends.</p>7</BaseLayout>
The page is now only what is different about it. A layout is an ordinary component: putting it in src/layouts/ is a convention, not a rule, and it takes props and slots like any other.
QuizInteractive · disabled in the PDF
A page uses <BaseLayout title="Recipes">, but its <h1> and list never appear. What is the most likely cause?
A page uses <BaseLayout title="Recipes">, but its <h1> and list never appear. What is the most likely cause?
Astro80 / 212
Module 9 · Layouts and the page head
The head tells search engines and social sites what the page is
Metadata is information about the page, not on it. Search engines read the title and description. A canonical URL names the one official address of the page, when the same page can be reached several ways. Social sites read Open Graph tags (og:…) to draw link previews.
Astro.site is the site you set in astro.config.mjs (https://example.com), as a URL. Astro.url is the address of the page being rendered. Joined, they give the page’s full public address.
Set `site`. Without site in the config, Astro.site is undefined, and new URL("/about", undefined) throws. Anything that needs full addresses (canonical links, sitemaps, RSS feeds) needs site.
aria-current="page" tells screen readers which link is the current page, and gives CSS a hook: a[aria-current="page"] { font-weight: 700; }. An attribute set to undefined isn’t written at all.
Find the bugInteractive · disabled in the PDF
On /recipes/soup/, the Recipes link should be highlighted too, but it isn’t. Which line should change?
Astro
Astro82 / 212
Module 9 · Layouts and the page head
Layouts can wrap layouts
Recipe pages share more than the site frame: a title, the time and servings, the tags. A RecipeLayout can hold that, and itself sit inside BaseLayout. Each layer adds its part and passes the content on.
Make a PostLayout that sits inside BaseLayout, passes the title on, and places the post.
Astro
1---2import BaseLayout from'./BaseLayout.astro'3const{ title }= Astro.props4---5<title={}>6<article>7<h1>{title}</h1>8</>9</article>10</BaseLayout>
Astro83 / 212
Module 9 · Layouts and the page head
Let pages add to the head
Some pages need something extra in <head>: a picture for link previews, or a noindex tag on a draft. A named slot in the head (module 6) lets them add it without the layout knowing every case.
A .md file in src/pages/ is a page too: Astro turns the Markdown into HTML. Its frontmatter (the block between --- lines at the top, in YAML) can name a layout. The layout receives the values as Astro.props.frontmatter, and the rendered Markdown in its slot.
src/pages/now.md
---layout: ../layouts/MarkdownLayout.astrotitle: What I’m cooking now---Mostly soups. It is that time of year.
Only for Markdown pages. The layout key works for Markdown files in src/pages/. Posts in a content collection (next module) are rendered by a page you write, which picks the layout itself.
Going deeper
A Markdown layout also receives headings, url and file as props. For one-off pages like now.md, Markdown pages are handy. For many posts of the same shape, a content collection checks every file against a schema, which is why the blog uses one.
Astro85 / 212
Module 9 · Layouts and the page head
Recap: layouts
A layout is a component holding the frame of pages, with a <slot /> for the content.
Put lang, charset, viewport, title and description in the layout’s <head>.
Canonical and social tags need full addresses: new URL(Astro.url.pathname, Astro.site).
Astro.url.pathname lets the navigation mark the current page with aria-current.
Layouts nest: a recipe layout inside the base layout.
A named head slot lets pages add their own tags.
Markdown pages in src/pages/ choose a layout with layout: in their frontmatter.
FlashcardsInteractive · disabled in the PDF
Layout
A component with the shared frame of pages and a slot for their content.
Metadata
Information about a page, in its <head>: title, description, canonical, social tags.
Canonical URL
The one official address of a page, given with <link rel="canonical">.
Astro.site
The site from the config, as a URL; undefined if not set.
Astro.url
The address of the page being rendered, as a URL.
aria-current="page"
Marks the link to the current page, for screen readers and CSS.
Frontmatter (Markdown)
YAML between --- lines at the top of a Markdown file.
Try itInteractive · disabled in the PDF
Module 10: Markdown and content collections
Astro86 / 212
Module 10 · Markdown and content collections
Markdown is writing with a few symbols
Markdown is a way to write formatted text in a plain file: a # makes a heading, * a list item, **…** bold, [text](url) a link. It is easier to write than HTML, and Astro turns it into HTML for you.
src/content/recipes/tomato-soup.md
---title: Tomato souppubDate: 2026-10-02tags: [soup, quick]servings: 4minutes: 30---## You need- 1 kg tomatoes- 1 onionRoast the tomatoes **until soft**.
Becomes (the body)
<h2id="you-need">You need</h2><ul><li>1 kg tomatoes</li><li>1 onion</li></ul><p>Roast the tomatoes<strong>until soft</strong>.</p>
The block between the --- lines is frontmatter: facts about the recipe, in YAML (a simple key: value format). The rest is the body, the text itself.
Markdown
Plain text with a few symbols for headings, lists, links and emphasis.
YAML
A simple data format of key: value lines, used for frontmatter.
Body
The text of a Markdown file, after the frontmatter.
Astro87 / 212
Module 10 · Markdown and content collections
A collection is a set of files of the same shape
Forty recipes should all have a title, a date and a number of servings. Without a check, one file with servings: "four" or a missing date breaks a page, and you find out in production. A content collection groups files of one kind, checks each against a schema (a description of the shape the data must have), and gives you typed functions to query them.
One place says what a recipe is.
The build stops with a clear message on a bad file, naming the file and the field.
Your editor knows recipe.data.minutes is a number.
Content collection
A named group of entries of one shape, loaded and checked by Astro.
Schema
A description of the shape data must have; data that doesn’t fit is rejected.
Lines 1-3: Three imports: the collection helper, the file loader, and Zod, the schema library.
Lines 6: A loader says where entries come from. glob reads every file matching the pattern under base.
Lines 7-14: The schema: what each recipe’s frontmatter must contain, and of what type.
Lines 9:coerce.date() turns the text 2026-10-02 into a real Date.
Lines 10,13:default fills in a value when a file leaves the field out.
Lines 17: Export every collection by name in collections.
Older tutorials look different. Before the current content layer, collections lived in src/content/config.ts, used type: "content", and entries had slug and entry.render(). In Astro 7 these only work behind the temporary legacy.collectionsBackwardsCompat flag. Import z from astro/zod: the z from astro:content is deprecated and goes away in Astro 8.
Astro89 / 212
Module 10 · Markdown and content collections
Zod in five minutes
Zod is a library for describing data. You build a schema from small pieces, and Zod checks a value against it: either it fits (and you get it back, typed) or you get a list of what is wrong.
Piece
Accepts
z.string()
Text
z.number().int().positive()
Whole numbers above zero
z.array(z.string())
A list of texts
z.enum(["easy", "hard"])
Only one of those words
.optional()
The field may be missing
.default(x)
Missing means x
QuizInteractive · disabled in the PDF
A recipe has servings: four in its frontmatter. With the schema from the last slide, what happens?
A recipe has servings: four in its frontmatter. With the schema from the last slide, what happens?
Astro90 / 212
Module 10 · Markdown and content collections
getCollection lists entries
src/pages/recipes/index.astro
1---2import{ getCollection }from'astro:content'3import BaseLayout from'../../layouts/BaseLayout.astro'45constrecipes=(6awaitgetCollection('recipes',({ data })=>!data.draft)7).sort((a, b)=> b.data.pubDate.valueOf()- a.data.pubDate.valueOf())8---9<BaseLayouttitle="Recipes">10<ul>11{recipes.map((r)=>(12<li><ahref={`/recipes/${r.id}/`}>{r.data.title}</a></li>13))}14</ul>15</BaseLayout>
Each entry has an id (from the file name: tomato-soup), its checked frontmatter as data, and its body.
The second argument is a filter: here, drafts are left out.
The order isn’t guaranteed, so sort it yourself: newest first.
await waits for the result: the frontmatter runs on the server, so it can wait.
Astro91 / 212
Module 10 · Markdown and content collections
Practise: list the quick recipes
Fill the blanksInteractive · disabled in the PDF
List the published recipes tagged quick, sorted by title.
Lines 2:render comes from astro:content, next to the queries.
Lines 5-11: One page per recipe, each carrying its entry as a prop (module 8).
Lines 14:render turns the body into a Content component, and lists the headings (for a table of contents).
Lines 16: Spread data into the layout: title, minutes and servings match its props.
Lines 17:<Content /> puts the rendered Markdown in the page.
Find the bugInteractive · disabled in the PDF
This page comes from an old tutorial and fails in Astro 7. Which line?
Astro
Astro93 / 212
Module 10 · Markdown and content collections
Tag pages from the collection
The tags pages from module 8 now come from the recipes themselves: every tag that appears in any frontmatter gets its page, and a new tag in a new file makes a new page at the next build.
src/pages/tags/[tag].astro (frontmatter)
1exportasyncfunctiongetStaticPaths(){2const recipes =awaitgetCollection(3'recipes',({ data })=>!data.draft)4const tags =newSet(recipes.flatMap((r)=> r.data.tags))5return[...tags].map((tag)=>({6params:{ tag },7props:{8recipes: recipes.filter((r)=> r.data.tags.includes(tag)),9},10}))11}
Keep tags in one form. Tags are part of the address, so Soup and soup would be two pages. Lowercase them in the schema with z.string().toLowerCase() and every file agrees.
Astro94 / 212
Module 10 · Markdown and content collections
Data files and references between collections
Not every collection is Markdown. The file() loader reads one JSON or YAML file holding many entries, each with an id. A reference() field points from one collection to another and checks the target exists.
src/content.config.ts (more)
1import{ defineCollection, reference }from'astro:content'2import{ file, glob }from'astro/loaders'3import{ z }from'astro/zod'45const authors =defineCollection({6loader:file('src/data/authors.json'),7schema: z.object({name: z.string(),bio: z.string()}),8})910const blog =defineCollection({11loader:glob({pattern:'**/*.md',base:'./src/content/blog'}),12schema: z.object({13title: z.string(),14pubDate: z.coerce.date(),15author:reference('authors'),16}),17})
In a post, author: sam must name an entry in authors.json. The entry stores a pointer, { collection, id }, not the author: fetch them with await getEntry(post.data.author) when you need them.
QuizInteractive · disabled in the PDF
What is post.data.author after loading the collection?
What is post.data.author after loading the collection?
Going deeper
A loader can also be a function that returns entries from anywhere (an API, a database), or an object with a load method that keeps its own store and syncs only what changed: that is how CMS loaders are written. These run when content is synced, at build time or when astro dev starts. For data that must be fresh on every request, live collections are defined in src/live.config.ts and queried with getLiveCollection and getLiveEntry in on-demand routes (module 13).
Astro95 / 212
Module 10 · Markdown and content collections
Recap: content collections
Markdown is text with symbols; frontmatter (YAML) holds the facts, the body the text.
Collections live in src/content.config.ts: a loader and a Zod schema each.
glob() reads many files, file() one JSON or YAML file of entries.
A bad file stops the build with the file and field named.
getCollection(name, filter) lists entries: id, data, body. Sort them yourself.
render(entry) gives Content and headings; getEntry fetches one entry.
reference() links collections with a checked pointer.
FlashcardsInteractive · disabled in the PDF
Content collection
A named group of entries of one shape, loaded and checked by Astro.
src/content.config.ts
Where collections are declared and exported as collections.
Loader
Says where a collection’s entries come from: glob(), file(), or your own.
Zod
A schema library: describes data and checks values against it. Import from astro/zod.
getCollection
Returns every entry of a collection, optionally filtered.
render(entry)
Renders an entry’s body: returns Content and headings.
reference("authors")
A field holding a checked pointer to another collection’s entry.
z.coerce.date()
Accepts a date written as text and turns it into a Date.
Try itInteractive · disabled in the PDF
Module 11: Images, fonts and other files
Astro96 / 212
Module 11 · Images, fonts and other files
Images are usually the heaviest part of a page
A photo straight from a phone can weigh 4 MB and be 4000 pixels wide. On a recipe card it is shown 400 pixels wide. The visitor downloads ten times more than they see, and on a slow connection the page feels broken while it arrives.
Size: send an image close to the width it is shown at.
Format: modern formats like WebP and AVIF are much smaller than JPEG or PNG for the same look.
Dimensions: tell the browser the width and height up front, so it can keep the space free.
Timing: load images far down the page only when the visitor gets near them.
Image format
How the picture is stored in a file: JPEG, PNG, WebP, AVIF, SVG. Each compresses differently.
Optimising
Resizing and converting an image so the file is as small as it can be while still looking right.
Layout shift
Content jumping on screen as things above it finish loading. It makes people click the wrong thing.
Astro97 / 212
Module 11 · Images, fonts and other files
public/ is copied as it is; src/ is processed
public/
favicon.svg# served at /favicon.svg, untouched
robots.txt
src/
assets/
pancakes.jpg# imported, resized, renamed
content/
recipes/
pancakes.md
public/
src/ (imported)
How you refer to it
By its address: /favicon.svg
By import, like code
What the build does
Copies it as it is
Optimises it, gives it a hashed name
A missing file
A broken image, found by a visitor
A build error, found by you
Good for
Favicons, robots.txt, files that need a fixed address
Photos and pictures in your pages
A safe default. Put pictures you show in pages under src/ (the convention is src/assets/). Use public/ only for files whose address must never change.
Astro98 / 212
Module 11 · Images, fonts and other files
The Image component does the work for you
WalkthroughInteractive · disabled in the PDF
src/components/RecipeCard.astro
1---2import{ Image }from'astro:assets'3import pancakes from'../assets/pancakes.jpg'4---56<article>7<Image8src={pancakes}9alt="A stack of pancakes with berries"10width={400}11/>12<h2>Weekend pancakes</h2>13</article>
Lines 2:Image comes from astro:assets, a module Astro provides.
Lines 3: Importing the file gives an object with its address, width, height and format, checked at build time.
Lines 8: Pass the imported object, not a string.
Lines 9:alt is required: a description for people using screen readers, and shown if the image fails.
Lines 10: Ask for 400 pixels wide. Astro keeps the proportions and works out the height.
HTML
1<imgsrc="/_astro/pancakes.Bx3k9.webp"alt="A stack of pancakes2 with berries"width="400"height="267"loading="lazy"3decoding="async">
What reaches the browser (shortened): a WebP file, its size, and lazy loading.
QuizInteractive · disabled in the PDF
Why does Astro write width and height on the <img> even though CSS may resize it?
Why does Astro write width and height on the <img> even though CSS may resize it?
Astro99 / 212
Module 11 · Images, fonts and other files
Picture offers several formats; the browser picks one
Astro
1---2import{ Picture }from'astro:assets'3import hero from'../assets/kitchen.jpg'4---5<Picture6src={hero}7formats={['avif','webp']}8alt="Sam's kitchen table, set for breakfast"9width={1200}10priority11/>
<Picture> writes a <picture> element with one <source> per format and a plain <img> fallback. A browser takes the first format it understands. <Image> alone gives WebP by default, which every current browser reads.
priority loads the image eagerly and early: use it for the one big image at the top of a page. If every image is a priority, none is.
Everything else stays loading="lazy": fetched only as the visitor scrolls near it.
quality (low, mid, high, max, or a number) trades file size for detail.
Find the bugInteractive · disabled in the PDF
This card stops the build with an image error. Which line is wrong?
Astro
Astro100 / 212
Module 11 · Images, fonts and other files
Responsive images send each screen the size it needs
A phone needs a smaller file than a wide desktop screen. A responsive image lists several widths in srcset, and the browser downloads only the one that fits. Astro writes that list for you when you choose a layout.
Shrinks with small screens, never grows past its width
full-width
Always fills the width of its container
fixed
Stays at its width, with sharper copies for high-density screens
A layout prop on one <Image> overrides the config. Set image.responsiveStyles: true and Astro also adds a little CSS so each image behaves as its layout says; it is off by default, so either turn it on or style the images yourself.
Going deeper
You can also pass widths (a list of pixel widths) and sizes (how wide the image is shown at each screen size), or densities like [1, 2] for fixed-size images. widths and densities can’t be used together.
Astro101 / 212
Module 11 · Images, fonts and other files
Images from other sites must be allowed first
A remote image lives on another website, given by its full address. Astro can’t import it, so it doesn’t know its size: pass width and height, or inferSize to have Astro fetch it and measure. To have Astro optimise it, list its host in the config. Otherwise it is shown as it is.
1<Image2src="https://images.example.org/bread.jpg"3alt="A loaf of sourdough bread"4width={600}5height={400}6/>
QuizInteractive · disabled in the PDF
Why must you list the hosts whose images Astro may optimise?
Why must you list the hosts whose images Astro may optimise?
Astro102 / 212
Module 11 · Images, fonts and other files
Images in content collections are checked too
A recipe’s cover is named in its frontmatter. The schema can say it is an image with the image() helper. Astro then finds the file next to the Markdown, checks it exists, and hands your page an imported image, ready for <Image>.
Fill the blanksInteractive · disabled in the PDF
Make cover a checked image in the recipes collection.
A web font is a file the browser downloads to draw text in a typeface it doesn’t have. Loading it from another company’s server costs an extra connection and shares visitors’ addresses with them. Astro’s fonts API (stable since Astro 6) fetches the files at build time and serves them from your own site.
1---2import{ Font }from'astro:assets'3---4<head>5<FontcssVariable="--font-lora"preload/>6</head>7<styleis:global>8body{font-family:var(--font-lora),serif;}9</style>
<Font> writes the @font-face rules; preload asks the browser to fetch the file early.
Fallback fonts that fit. While a font downloads, text is drawn in a fallback. Astro generates a fallback adjusted to the web font’s proportions, so the swap doesn’t move lines around: the same layout-shift problem as images, solved for text.
Astro104 / 212
Module 11 · Images, fonts and other files
Other files: ?url and ?raw
Astro
1---2import printable from'../assets/pancakes.pdf?url'3import logo from'../assets/logo.svg?raw'4---5<ahref={printable}download>Print the recipe</a>6<spanset:html={logo}/>
?url copies the file into the build with a hashed name and gives you its address.
?raw gives you the file’s contents as a string, here an SVG drawn inline.
A plain import of an .svg gives an Astro component: <Logo width={32} />.
QuizInteractive · disabled in the PDF
Sam wants /recipes.pdf to keep that exact address forever, because it is printed on flyers. Where does it go?
Sam wants /recipes.pdf to keep that exact address forever, because it is printed on flyers. Where does it go?
Astro105 / 212
Module 11 · Images, fonts and other files
Recap: images, fonts and files
Images in src/ are imported, checked and optimised; public/ is copied as it is.
<Image> resizes, converts to WebP, writes width and height, and lazy loads.
<Picture> offers several formats; priority is for the first big image only.
A layout makes images responsive: several widths, the browser picks.
Remote images need their size, and their host listed to be optimised.
image() in a collection schema checks covers exist.
The fonts API serves fonts from your site; ?url and ?raw cover other files.
Try itInteractive · disabled in the PDF
FlashcardsInteractive · disabled in the PDF
<Image>
From astro:assets: resizes, converts and sizes an image to avoid layout shift.
<Picture>
Several formats in a <picture> element; the browser picks the first it supports.
Layout shift
Content jumping as things above it load; avoided by giving sizes up front.
priority
Load this image eagerly and early: for the main image above the fold.
image.domains
Hosts whose remote images Astro may optimise.
image()
Schema helper: a frontmatter path checked and turned into image metadata.
?url
Copies a file into the build with a hashed name and returns its address.
<Font>
Writes @font-face rules for a font set up in the fonts config.
Module 12: Islands and client directives
Astro106 / 212
Module 12 · Islands and client directives
By default, Astro sends no JavaScript
Everything you have built so far runs once, when the page is made, and reaches the browser as HTML and CSS. A recipe page with a title, a photo and a list of steps needs nothing else. Browsers draw HTML fast; JavaScript has to be downloaded, read and run first, which is what makes many sites slow on phones.
Some parts do need to react to the visitor: a search box that filters recipes as you type, a "save recipe" button, a timer. Astro lets those parts, and only those, run in the browser.
A page is a sea of static HTML with a few islands of interactivity.
Island
A component that runs in the browser, inside a page that is otherwise static HTML.
Islands architecture
Building pages as static HTML with separate, independent interactive islands.
QuizInteractive · disabled in the PDF
Which parts of a recipe page need to run in the browser?
Which parts of a recipe page need to run in the browser?
Astro107 / 212
Module 12 · Islands and client directives
Hydration brings server HTML to life
An island is rendered twice. First on the server, to HTML, so the visitor sees it straight away. Then, in the browser, its JavaScript runs, renders it again, finds the HTML already there, and attaches the event handlers. That second step is hydration: the dry HTML gets water, and starts responding to clicks.
Visible is not the same as working. Between the first and last step the search box is on screen but does nothing. Choosing when each island hydrates (next slides) is choosing how long that gap is, and how much it costs.
Hydration must match. The browser’s first render must produce exactly the server’s HTML. Date.now(), Math.random(), localStorage or window.innerWidth read during render give different answers in each place, and React reports a mismatch and redraws. Read browser-only values in useEffect, after hydration.
Shortened. The custom element holds what the island needs to wake up.
<astro-island> is a custom element: an HTML tag defined by a small script Astro adds.
It waits for its directive’s moment, loads the component and the renderer, reads props, and hydrates.
React is downloaded once however many islands use it; each island adds only its own code.
Astro108 / 212
Module 12 · Islands and client directives
Islands are written with a UI framework
Astro components never run in the browser, so islands are written with a UI framework: React, Preact, Svelte, Vue or Solid. This course uses React. A quick reminder: a React component is a function that returns JSX (HTML-like markup in JavaScript), and useState gives it a value that, when changed, draws the component again.
Terminal
1npxastroaddreact
Installs @astrojs/react (the renderer), React itself, and adds react() to integrations.
timeout stops idle waiting forever; rootMargin starts visible a little early.
Astro112 / 212
Module 12 · Islands and client directives
Check: choose the directive
QuizInteractive · disabled in the PDF
A likes counter sits at the very bottom of long recipe pages. Most visitors never scroll that far. Which directive?
A likes counter sits at the very bottom of long recipe pages. Most visitors never scroll that far. Which directive?
QuizInteractive · disabled in the PDF
What do you lose with client:only="react"?
What do you lose with client:only="react"?
Astro113 / 212
Module 12 · Islands and client directives
Props cross from server to browser as data
Astro writes an island’s props into the HTML, then reads them back in the browser. This is serialising: turning values into text that can travel. Strings, numbers, booleans, arrays and plain objects work, and so do Date, Map, Set, URL and BigInt. Functions don’t: code in the server’s memory can’t be written into a page.
Find the bugInteractive · disabled in the PDF
This island breaks. Which line causes it?
Astro
Props are public. Everything in an island’s props is in the page source, readable by anyone. Pass only what the island needs: a recipe’s slug and title, not a whole database row with private notes.
Astro114 / 212
Module 12 · Islands and client directives
Islands are separate: share state through a module
Each island is its own React root, hydrated on its own. A React context in one doesn’t reach another, and you can’t lift state up into their common parent: that parent is an Astro page, which exists only on the server. What islands can share is a JavaScript module both import, because the browser loads a module once per page.
src/stores/saved.ts
1import{ atom }from'nanostores'23/** The slugs of recipes saved on this page visit. */4exportconst $saved =atom<string[]>([])
The Save island calls $saved.set(...); this island, elsewhere on the page, updates.
Store
A small object holding a value, that code can read, change and subscribe to. Nano Stores is a tiny library for them.
Astro115 / 212
Module 12 · Islands and client directives
Recap: islands
Pages send no JavaScript unless a component asks for it with a client directive.
An island is rendered to HTML first, then hydrated in the browser.
Pick the latest moment that still feels right: load, idle, visible, media.
client:only skips the server HTML: only for code that can’t run on a server.
Props are serialised into the page: data only, nothing private, no functions.
Islands share state through a module, such as a Nano Store.
Try itInteractive · disabled in the PDF
FlashcardsInteractive · disabled in the PDF
Island
A component that runs in the browser, in an otherwise static page.
Hydration
Running a component in the browser over its server HTML, attaching events.
client:load
Hydrate as soon as the page loads.
client:idle
Hydrate when the browser is idle (optionally with a timeout).
client:visible
Hydrate when it scrolls into view (optionally with a rootMargin).
client:media
Hydrate when a CSS media query matches.
client:only
Skip the server render; render only in the browser, with the named framework.
Serialising
Turning values into text, so props can be written into the HTML.
<astro-island>
The custom element that loads and hydrates one island.
Module 13: Rendering modes, adapters and deploying
Astro116 / 212
Module 13 · Rendering modes, adapters and deploying
A page can be made at build time or at request time
So far every page has been prerendered: made once, during astro build, and saved as an HTML file. Every visitor gets the same file. That is fast and cheap, and right for a recipe that changes once a month.
Some pages can’t be made ahead of time, because they depend on who is asking or on what is true right now: "your saved recipes", search results for any word, a page that shows today’s comments. Those are rendered on demand: a server runs your page’s code for each request and sends back fresh HTML.
Prerendered (static)
Runs once, in astro build
The same HTML for everyone
Served as a file: fast, cheap, easy to host
Changes need a new build
On demand
Runs for every request, on a server
Can differ per visitor and per moment
Needs a server running your code
Always up to date
Prerender
Render a page to HTML at build time, ahead of any visitor.
On-demand rendering
Render a page when a request arrives, on a server. Also called server-side rendering (SSR).
Astro117 / 212
Module 13 · Rendering modes, adapters and deploying
Most sites keep the default and turn on demand on only for the few routes that need it. Sam’s blog stays static, and only /saved runs per request.
QuizInteractive · disabled in the PDF
Which pages of Sam’s blog need on-demand rendering?
Which pages of Sam’s blog need on-demand rendering?
Astro118 / 212
Module 13 · Rendering modes, adapters and deploying
Some things exist only when there is a request
In a page
Prerendered
On demand
Astro.url, Astro.params
The address being built
The real request’s
Astro.request.headers
Empty: there is no visitor
The visitor’s headers
Astro.cookies
Nothing to read
Read and set
Astro.clientAddress
Not available
The visitor’s IP address
Astro.isPrerendered
true
false
Cookie
A small named value a site asks the browser to keep and send back with every later request to that site.
Header
A named line of extra information sent with a request or a response, like Accept-Language or Cookie.
A static page has no visitor. Code in a prerendered page runs once, for nobody in particular. Anything personal (who is signed in, their saved recipes, their language) has to happen in an on-demand route, or in the browser, in an island.
Astro119 / 212
Module 13 · Rendering modes, adapters and deploying
Check: personal data on a static page
QuizInteractive · disabled in the PDF
The prerendered home page shows Hello, {name}, with name read from Astro.cookies. What do visitors see once it is deployed?
The prerendered home page shows Hello, {name}, with name read from Astro.cookies. What do visitors see once it is deployed?
Astro120 / 212
Module 13 · Rendering modes, adapters and deploying
An adapter turns the build into a server for one platform
On-demand routes need a server to run on, and every hosting platform runs code differently: a Node.js process, a serverless function, an edge worker. An adapter is an integration that builds your on-demand routes into the shape one platform expects. Without one, astro build refuses on-demand routes.
Terminal
1npxastroaddnode# your own server, or any Node host2npxastroaddnetlify3npxastroaddvercel4npxastroaddcloudflare
standalone builds a server you start with node ./dist/server/entry.mjs.
An adapter makes on demand possible, not the default. Adding an adapter doesn’t change output. With the default static, only routes that export prerender = false run per request; everything else is still a file in the build.
Astro121 / 212
Module 13 · Rendering modes, adapters and deploying
Check: what a hybrid build produces
Put in orderInteractive · disabled in the PDF
A visitor opens /saved on a static-default site with the Node adapter. Put what happens in order.
1.The route reads the cookie and renders HTML
2.It runs the saved.astro route’s code with the real request
3.The browser sends a request for /saved, with its cookies
4.The server sends that HTML back
5.The server finds no prerendered file for /saved
Astro122 / 212
Module 13 · Rendering modes, adapters and deploying
Server islands: a static page with one fresh part
Often a page is static except for one small part: a recipe page with "3 people saved this today". Making the whole page on demand would lose the speed of a file. A server island lets the page stay prerendered while one Astro component is rendered on demand, after the page arrives.
The page is sent with the fallback content in place.
A small script asks the server for that component’s HTML, with its props.
The server renders it fresh (it can read cookies) and the script swaps it in.
Client island (`client:*`)
Runs in the browser
Ships a framework’s JavaScript
For things the visitor interacts with
Server island (`server:defer`)
Renders on the server, per request
Ships only HTML
For fresh or personal content to show
Note. Server islands need an adapter, since something has to render them on request.
Astro123 / 212
Module 13 · Rendering modes, adapters and deploying
Sessions remember a visitor between requests
A session keeps data about one visitor on the server, between their requests. The browser only holds a cookie with a random id; the data itself (a cart, saved recipes) lives in storage the adapter provides or you configure. Sessions work only in on-demand routes.
A driver says where session data is kept. Some adapters set one up for you; session: false turns sessions off.
Astro124 / 212
Module 13 · Rendering modes, adapters and deploying
Deploying: a folder of files, or a server
Deploying means putting your built site on a computer connected to the internet, a host, so anyone can visit it. What you upload depends on the mode.
Your site
astro build writes
Host it on
All static (no adapter)
dist/: HTML, CSS, images, scripts
Any static host: just upload the folder
Some on demand (adapter)
Static files and server code, in the adapter’s format
The platform the adapter is for
npm run build makes the production build.
npm run preview (astro preview) serves that build on your computer, to check it before going live.
Most hosts connect to your Git repository and run the build themselves on every push.
Check the build, not only the dev server.astro dev renders every page again each time you open it, even prerendered ones, so a time or fetched data looks fresh on every refresh there. (It does give prerendered pages a request without cookies, headers or query, as the build does, and warns in the terminal if you read the headers.) astro preview shows the real behaviour.
QuizInteractive · disabled in the PDF
A prerendered page shows the time it was rendered. Under astro dev the time changes on every refresh. What do visitors see once it is deployed?
A prerendered page shows the time it was rendered. Under astro dev the time changes on every refresh. What do visitors see once it is deployed?
Astro125 / 212
Module 13 · Rendering modes, adapters and deploying
Recap: rendering modes and deploying
Prerendered pages are made once at build time; on-demand pages are made per request.
The default is static; prerender = false makes one route on demand.
Request data (cookies, headers, IP) only exists on demand.
An adapter builds on-demand routes for one platform.
Server islands keep a page static with one fresh part.
Sessions keep per-visitor data on the server; only on demand.
A static site is a folder for any host; test with astro preview.
Try itInteractive · disabled in the PDF
FlashcardsInteractive · disabled in the PDF
Prerender
Render to HTML at build time, the same for every visitor.
On-demand rendering
Render per request on a server; can differ per visitor.
export const prerender = false
Make this route on demand on a static-default site.
Adapter
An integration that builds on-demand routes for one hosting platform.
server:defer
A server island: rendered on demand after the static page loads.
Session
Per-visitor data kept on the server, found by an id in a cookie.
astro preview
Serves the production build locally, to test it as it will run.
Deploying
Putting the built site on a host connected to the internet.
Module 14: Endpoints, middleware and environment variables
Astro126 / 212
Module 14 · Endpoints, middleware and environment variables
An endpoint is a route that returns anything
A page returns HTML. Sometimes a site needs to answer with something else: a JSON list of recipes for the search island, an RSS feed for news readers, a robots.txt. An endpoint is a .ts (or .js) file in src/pages/ that exports a function per HTTP method, and returns a Response.
recipes.json.ts becomes /recipes.json: the .ts is dropped, the rest is the address.
HTTP method
The verb of a request: GET asks for something, POST sends something, and there are PUT, PATCH, DELETE.
JSON
A text format for data, made of objects, arrays, strings, numbers, true, false and null.
`Response`
The web’s standard object for an answer: a body, a status code and headers.
Astro127 / 212
Module 14 · Endpoints, middleware and environment variables
Prerendered endpoints become files
Endpoints follow the same rendering rules as pages. On a static site, GET runs once during the build and its body is saved as a file: dist/recipes.json. On demand, it runs for every request, and other methods like POST work too.
Dynamic endpoints use getStaticPaths exactly like dynamic pages.
QuizInteractive · disabled in the PDF
On a fully static site with no adapter, Sam adds export const POST to likes.ts. What happens when a visitor sends a POST?
On a fully static site with no adapter, Sam adds export const POST to likes.ts. What happens when a visitor sends a POST?
Astro128 / 212
Module 14 · Endpoints, middleware and environment variables
The context: everything about the request
WalkthroughInteractive · disabled in the PDF
src/pages/api/likes.ts
1importtype{ APIRoute }from'astro'23exportconst prerender =false45exportconstPOST:APIRoute=async({ request, cookies })=>{6const body =await request.json()7if(typeof body.slug !=='string'){8returnnewResponse('A slug is needed',{status:400})9}10const total =awaitaddLike(body.slug)11 cookies.set('liked', body.slug,{path:'/',httpOnly:true})12return Response.json({ total },{status:201})13}
Lines 3: Accepting posts means running per request.
Lines 5: The function receives a context object: request, params, url, cookies, locals, redirect, rewrite…
Lines 6: Read the body the browser sent, as JSON.
Lines 7-9: Never trust input. A 400 status means the request was wrong.
Lines 10:addLike stands for your own storage code: a database, say.
Lines 11:httpOnly keeps the cookie out of reach of page scripts.
Lines 12:201 means something was created.
Status
Means
200
OK
201
Created
301, 302, 303
Go somewhere else (a redirect)
400
The request was wrong
401, 403
Not signed in, or not allowed
404
Not found
500
The server failed
Astro129 / 212
Module 14 · Endpoints, middleware and environment variables
Middleware runs before every route
Middleware is a function in src/middleware.ts that Astro calls for every request, before the page or endpoint. It can look at the request, add information for the route, answer by itself, or change the response on the way out.
src/middleware.ts
1import{ defineMiddleware }from'astro:middleware'23exportconst onRequest =defineMiddleware(async(context, next)=>{4const started = Date.now()5const response =awaitnext()6const ms = Date.now()- started7 console.log(`${context.url.pathname} took ${ms} ms`)8return response9})
The request reaches the middleware first.
await next() renders the matched route.
Back in the middleware, the response can be read or changed.
The middleware returns it. Returning early, without next(), skips the route.
Code before next() sees the request; code after it sees the response.
Astro130 / 212
Module 14 · Endpoints, middleware and environment variables
locals carry information to the route
locals is an object that lives for one request. Middleware fills it; pages read Astro.locals, endpoints context.locals. Typing it once in src/env.d.ts makes it typed everywhere.
src/middleware.ts
1import{ defineMiddleware }from'astro:middleware'23exportconst onRequest =defineMiddleware((context, next)=>{4const units = context.cookies.get('units')?.value5 context.locals.units = units ==='imperial'?'imperial':'metric'6returnnext()7})
In an on-demand recipe page, read the visitor’s units.
Astro
1---2exportconst prerender =false3const units =.units4---5<p>Quantities in {units}</p>
Astro131 / 212
Module 14 · Endpoints, middleware and environment variables
On static pages, middleware runs at build time
For prerendered routes, middleware runs during astro build, once per page, with no real visitor. It does not run later, when someone downloads the file. What it puts in locals is baked into the HTML.
QuizInteractive · disabled in the PDF
Middleware sends visitors without a member cookie away from /members/. The members pages are prerendered. What happens in production?
Middleware sends visitors without a member cookie away from /members/. The members pages are prerendered. What happens in production?
Protect on the server, per request. Anything private must be in an on-demand route that checks who is asking each time. A static file can be fetched by anyone who knows its address.
Astro132 / 212
Module 14 · Endpoints, middleware and environment variables
sequence(a, b) runs a, whose next() runs b, whose next() runs the route.
Astro133 / 212
Module 14 · Endpoints, middleware and environment variables
Environment variables keep settings out of the code
An environment variable is a named setting given to a program from outside: the address of a database, an API key, a feature switch. The same code gets different values on your computer and in production, and secrets never go into Git. Locally they usually sit in a .env file, which is kept out of Git.
1// In an island (browser code):2import{ COMMENTS_URL }from'astro:env/client'3// In an endpoint or on-demand page (server code):4import{ COMMENTS_TOKEN }from'astro:env/server'
Astro checks the values against the schema, and refuses to import a secret into browser code.
Going deeper
Older code reads import.meta.env.SOME_NAME. That still works: only names starting with PUBLIC_ reach browser code, and import.meta.env.DEV and PROD say which mode you are in. astro:env adds types and validation; validateSecrets: true also checks secrets when the build starts.
Astro134 / 212
Module 14 · Endpoints, middleware and environment variables
Check: where a secret may go
Find the bugInteractive · disabled in the PDF
The comments island must never expose the token. Which line is the problem?
TSX
Astro135 / 212
Module 14 · Endpoints, middleware and environment variables
Recap: endpoints, middleware and environment
An endpoint is a .ts file in src/pages/ exporting GET, POST… and returning a Response.
Prerendered endpoints become files; other methods need on demand and an adapter.
Middleware in src/middleware.ts wraps every route: before and after next().
locals carries per-request data from middleware to routes.
For prerendered pages, middleware runs only at build time.
Redirects change the address; rewrites serve other content at the same one.
astro:env types and checks settings, and keeps secrets on the server.
Try itInteractive · disabled in the PDF
FlashcardsInteractive · disabled in the PDF
Endpoint
A .ts or .js route exporting HTTP-method functions that return a Response.
APIRoute
The type of an endpoint function: (context) => Response.
Middleware
onRequest in src/middleware.ts, called for every route.
next()
Runs the rest of the chain and the route; returns the response.
locals
An object for one request, filled by middleware, read by routes.
sequence(a, b)
Combines middleware; a wraps b.
Rewrite
Serve another route’s content without changing the address.
Environment variable
A setting given from outside the code, different per environment.
astro:env/server
Server-only variables, secrets included; refused in browser code.
Module 15: Forms and actions
Astro136 / 212
Module 15 · Forms and actions
A form is the browser’s own way to send data
Long before JavaScript frameworks, the web had forms. A <form> collects what the visitor typed and, when they press the button, the browser sends it to an address as a new request. No script is needed.
Lines 1:method="POST" sends data to be acted on; action is the address it goes to.
Lines 2-5: A <label> around the input names it, for everyone, screen readers included.
Lines 4:name is the key the value is sent under. An input without a name is not sent at all.
Lines 4:type="email" and required let the browser check the value before sending.
Lines 6: A button inside a form submits it.
Form data
What a form sends: pairs of name and value. In JavaScript it is a FormData object.
Validation
Checking that input has the right shape (an email looks like an email) before acting on it.
Astro137 / 212
Module 15 · Forms and actions
Check: what a form sends
Find the bugInteractive · disabled in the PDF
The server always receives an empty email. Which line is wrong?
HTML
QuizInteractive · disabled in the PDF
The input has type="email" and required. Does the server still need to check the email?
The input has type="email" and required. Does the server still need to check the email?
Astro138 / 212
Module 15 · Forms and actions
An on-demand page can handle its own form
src/pages/newsletter.astro
1---2exportconst prerender =false34let message =''5if(Astro.request.method ==='POST'){6const data =await Astro.request.formData()7const email = data.get('email')8if(typeof email ==='string'&& email.includes('@')){9awaitsubscribe(email)10 message ='Thanks, you are on the list.'11}else{12 message ='That doesn’t look like an email.'13}14}15---16<formmethod="POST">17<inputtype="email"name="email"required/>18<button>Subscribe</button>19</form>20{message &&<p>{message}</p>}
A form without action posts back to its own address. The page runs again with a POST, reads the form data, and renders the answer. It works with no JavaScript, but you check every field by hand, and nothing is typed. Actions do that work for you.
Astro139 / 212
Module 15 · Forms and actions
An action is a typed server function
An action is a function that runs on the server, which forms and browser code can call. You describe its input with a schema (Zod, from astro/zod), and Astro checks every call against it before your code runs.
WalkthroughInteractive · disabled in the PDF
src/actions/index.ts
1import{ defineAction }from'astro:actions'2import{ z }from'astro/zod'34exportconst server ={5subscribe:defineAction({6accept:'form',7input: z.object({8email: z.email(),9weekly: z.boolean().optional()10}),11handler:async(input)=>{12awaitaddSubscriber(input.email, input.weekly ??false)13return{email: input.email }14}15})16}
Lines 4: All actions are exported from src/actions/index.ts as server.
Lines 5: The key, subscribe, is the action’s name.
Lines 6:accept: "form" takes form data; the default is JSON.
Lines 7-10: The input’s shape. A checkbox becomes a boolean: true when ticked.
Lines 11-14: Runs only with valid input, already typed. addSubscriber stands for your own storage.
Actions need a server. Actions run per request, so the site needs an adapter. Pages that only call them from an island can still be prerendered.
action={actions.subscribe} posts the form to the page with the action named in the address.
Astro runs the action, then renders the page; getActionResult returns { data } or { error }.
isInputError tells a validation failure apart; fields holds the messages for each field.
Fill the blanksInteractive · disabled in the PDF
Read the result of the subscribe action in the page.
TypeScript
1const result = Astro.(actions.)
Astro141 / 212
Module 15 · Forms and actions
Throw ActionError for problems the visitor should hear about
TypeScript
1import{ ActionError, defineAction }from'astro:actions'23handler:async(input)=>{4if(awaitisSubscribed(input.email)){5thrownewActionError({6code:'CONFLICT',7message:'This email is already subscribed.'8})9}10// ...11}
An ActionError has a code (named after an HTTP status: BAD_REQUEST, UNAUTHORIZED, NOT_FOUND, CONFLICT…) and a message safe to show. The caller gets it as result.error, never as a crash. Any other error thrown in the handler reaches the caller as an INTERNAL_SERVER_ERROR, still carrying its message, so catch failures that could reveal internals (a database error, say) and throw an ActionError with a message you chose.
QuizInteractive · disabled in the PDF
Which result does a visitor get when they submit email=hello (no @)?
Which result does a visitor get when they submit email=hello (no @)?
saveRecipe accepts JSON (the default), so it takes a plain object.
In the browser, actions.saveRecipe(...) sends a request to the server and resolves to { data, error }, typed from the action. No address to remember, no JSON to parse by hand. .orThrow() is the variant that returns the data or throws.
Astro143 / 212
Module 15 · Forms and actions
Progressive enhancement: works first, then better
Progressive enhancement means the basic version works with plain HTML, and JavaScript only improves it. A form with action={actions.subscribe} works even if scripts fail to load, are blocked, or haven’t arrived yet on a slow phone. An island can then take over the same form to answer without a page reload.
Island only
Nothing happens until the JavaScript loads
A script error breaks signing up
Needs onClick code for everything
Form first, island on top
Works from the first paint
Still works if scripts fail
The island adds instant feedback
Put in orderInteractive · disabled in the PDF
Put a no-JavaScript action form submission in order.
1.The handler runs with valid input
2.The browser shows the new page with the message
3.The visitor presses Subscribe
4.The browser posts the form data to the page
5.The page renders, reading getActionResult
6.Astro checks the data against the action’s schema
Astro144 / 212
Module 15 · Forms and actions
Astro checks where form posts come from
In a cross-site request forgery (CSRF), another website makes a visitor’s browser submit a form to your site, with the visitor’s cookies attached. Astro’s security.checkOrigin (on by default) compares where an on-demand POST came from with your site’s address, and answers 403 when they differ.
Validate every field on the server: actions do it with the schema.
Check who is asking inside the handler (context.locals, cookies), not only in the page.
Keep checkOrigin on. Action bodies are limited to 1 MB by default (security.actionBodySizeLimit).
Actions are public endpoints. Every action can be called by anyone who can send a request, not only by your form. Treat its handler like an API endpoint: it must check permissions itself. The handler’s second argument is the context, with cookies and locals.
Going deeper
After a form post, reloading the page asks the browser to post again. To avoid duplicate submissions, sites often redirect after a successful post (POST, then redirect, then GET). Astro’s getActionContext() lets middleware take over how form actions run, for patterns like keeping the result in a session across that redirect.
Astro145 / 212
Module 15 · Forms and actions
Recap: forms and actions
A form sends name/value pairs with method to action; no script needed.
Browser checks help visitors; only server checks protect you.
An action is a server function with a Zod input schema, in src/actions/index.ts.
accept: "form" for forms; getActionResult reads { data, error } in the page.
ActionError sends a safe, coded error; input errors list fields.
Islands call actions.name(...) directly, typed.
Build the form first and enhance it; keep checkOrigin on and check permissions in the handler.
Try itInteractive · disabled in the PDF
FlashcardsInteractive · disabled in the PDF
name attribute
The key an input’s value is sent under; inputs without one aren’t sent.
FormData
The JavaScript object for form data: data.get("email").
Action
A server function with a checked input, callable from forms and browser code.
defineAction
Declares an action: accept, input schema and handler.
Astro.getActionResult()
The result of a form-submitted action in the page: { data } or { error }.
ActionError
An error with a code and a message, sent to the caller as error.
Progressive enhancement
Plain HTML works first; JavaScript only makes it better.
CSRF
Another site making a visitor’s browser post to yours; checkOrigin blocks it.
Module 16: Languages and view transitions
Astro146 / 212
Module 16 · Languages and view transitions
A second language is a second set of addresses
Sam’s friends in Madrid want the recipes in Spanish. Making a site work in several languages is called internationalisation, shortened to i18n (an i, 18 letters, an n). Each language and region you support is a locale, named by a short code: en for English, es for Spanish, pt-br for Brazilian Portuguese.
There are two separate jobs. Translating means writing the words in each language: a person (or a tool) does that. Routing means deciding which address shows which language: /recipes/ in English, /es/recipes/ in Spanish. Astro helps with the routing. It never translates anything for you.
i18n
Internationalisation: making a site work in more than one language.
Locale
A language (and maybe region) the site supports, written as a code: en, es.
Default locale
The language a visitor gets when the address doesn’t say.
With the config above, where does the English about page live?
With the config above, where does the English about page live?
Astro148 / 212
Module 16 · Languages and view transitions
You still write the pages for each language
src/pages/
index.astro# /
about.astro# /about/
recipes/
[slug].astro# /recipes/lemon-cake/
es/
index.astro# /es/
about.astro# /es/about/
recipes/
[slug].astro# /es/recipes/lemon-cake/
Each Spanish page is a real file under src/pages/es/, named like the folder for its locale. To avoid writing every page twice, keep the page’s markup in a component (say RecipePage.astro) and let both files render it with a different locale prop. The files stay tiny; the design lives in one place.
Fill the blanksInteractive · disabled in the PDF
Complete the Spanish about page so it reuses the shared component.
Astro
1---2// src/pages//about.astro3import About from'../../components/About.astro'4---5<Aboutlocale=""/>
Astro.currentLocale is worked out from the address being rendered.
Pass the locale, don’t store it. A module-level “current language” variable works in one browser tab, but during a build or on a server it is shared by every page or visitor handled at once. Passing the locale as an argument (or a prop, for islands) can’t mix them up.
Going deeper
For translated Markdown, keep one folder per locale inside a collection (src/content/recipes/en/, src/content/recipes/es/). Entry ids then start with the locale, so getCollection("recipes", ({ id }) => id.startsWith("es/")) gives the Spanish recipes.
getRelativeLocaleUrl(locale, path) builds the address for a locale, following prefixDefaultLocale, base and trailingSlash. If you later move English under /en/, every link follows. getAbsoluteLocaleUrl does the same with the full site address in front.
Find the bugInteractive · disabled in the PDF
The config has prefixDefaultLocale: false. One link is broken. Which line?
Astro
Astro151 / 212
Module 16 · Languages and view transitions
Guessing the visitor’s language needs a request
Browsers send an Accept-Language header listing the languages the person reads. Astro.preferredLocale is the best match between that list and your locales, and Astro.preferredLocaleList is every match. Both need a real request, so they only work in routes rendered on demand (module 13). In a prerendered page there is no visitor yet: they are undefined.
A Spanish page that doesn’t exist yet sends the visitor to the English one instead of a 404.
QuizInteractive · disabled in the PDF
A prerendered home page redirects to /es/ when Astro.preferredLocale === "es". What happens for a visitor whose browser prefers Spanish?
A prerendered home page redirects to /es/ when Astro.preferredLocale === "es". What happens for a visitor whose browser prefers Spanish?
Astro152 / 212
Module 16 · Languages and view transitions
A view transition animates the change of page
When you follow a link, the browser normally blanks the old page and draws the new one. A view transition lets it animate between the two instead: a cross-fade, or a recipe photo that grows from its card into the header of the recipe page. Modern browsers can do this for ordinary page loads with two lines of CSS.
Both pages opt in; elements with the same view-transition-name on both pages morph into each other.
This is a cross-document view transition: real page loads, animated by the browser, with no JavaScript. Browsers without support simply navigate as before, so nothing breaks.
View transition
A browser animation between the old view of a page and the new one.
`view-transition-name`
A CSS name pairing an element on the old page with one on the new page, so it morphs.
Without the listener, the print button would work only on the first page visited.
Put in orderInteractive · disabled in the PDF
Put a ClientRouter navigation in order.
1.The old and new pages are swapped, animated by named elements
2.astro:after-swap fires, then the new page’s astro:page-load
3.The visitor clicks a link to another recipe
4.The router fetches the new page’s HTML
Astro154 / 212
Module 16 · Languages and view transitions
Choosing between CSS and the router
Native cross-document
Plain CSS, no JavaScript sent
Every navigation is a real page load
Works with a strict Content Security Policy
Can’t keep state alive between pages
`<ClientRouter />`
Swaps pages in place with JavaScript
transition:persist keeps players and islands alive
Scripts need astro:page-load to re-run
Not supported together with Astro’s CSP (module 18)
QuizInteractive · disabled in the PDF
Sam wants recipe photos to morph from the list into the recipe page, and the site has a strict CSP. What fits best?
Sam wants recipe photos to morph from the list into the recipe page, and the site has a strict CSP. What fits best?
Try itInteractive · disabled in the PDF
Astro155 / 212
Module 16 · Languages and view transitions
Recap: languages and transitions
i18n routing maps addresses to locales; it doesn’t translate or create pages.
With prefixDefaultLocale: false, the default language keeps the plain addresses.
Astro.currentLocale comes from the address; preferredLocale needs an on-demand route.
Build links with getRelativeLocaleUrl, never by hand.
Native view transitions are CSS and need no JavaScript.
<ClientRouter /> swaps pages in place; re-run setup code on astro:page-load.
FlashcardsInteractive · disabled in the PDF
i18n
Internationalisation: making a site work in several languages.
Locale
A supported language (and region) code, like en or pt-br.
prefixDefaultLocale
Whether the default language’s addresses get a prefix too.
Astro.currentLocale
The locale of the address being rendered.
Astro.preferredLocale
The best match for the visitor’s Accept-Language; on-demand routes only.
getRelativeLocaleUrl()
Builds an address for a locale from the i18n config.
i18n.fallback
Which locale to show when a page is missing in another.
transition:persist
Keeps an element or island alive across ClientRouter navigations.
astro:page-load
Fires after every ClientRouter navigation, and on the first load.
Module 17: Performance and caching
Astro156 / 212
Module 17 · Performance and caching
Measure what the visitor feels
“Fast” means three things to a visitor, and Google measures each as a Core Web Vital. Measure before you change anything: a guess about what is slow is usually wrong.
LCP
Largest Contentful Paint: how long until the biggest thing on screen (often the hero photo) is drawn. Aim under 2.5 s.
CLS
Cumulative Layout Shift: how much the page jumps around while loading. Aim under 0.1.
INP
Interaction to Next Paint: how long the page takes to respond after a tap or key press. Aim under 200 ms.
Lab and field data
Lab: a test run on your machine (Lighthouse in the browser’s DevTools). Field: numbers from real visitors.
Test the build, not the dev server.astro dev serves files one by one, unbundled and unoptimised, so its numbers mean little. Run astro build and astro preview, then measure that.
Astro157 / 212
Module 17 · Performance and caching
The fastest JavaScript is the JavaScript you don’t send
Astro sends no JavaScript unless a component asks. Each island adds its own code, its framework (once per page) and the work of hydrating it, which competes with the visitor’s first tap. That cost lands on INP and, on slow phones, on LCP.
Island on a recipe page
Sensible directive
Why
Search box in the header
client:idle
Wanted soon, but not before the recipe shows
Comments at the bottom
client:visible
Most visitors never scroll that far
Servings calculator
client:visible
Below the ingredients list
Share links
none: plain <a> tags
Links need no JavaScript at all
QuizInteractive · disabled in the PDF
Every island on the recipe page uses client:load. The page feels slow to respond on a cheap phone. What is the best first change?
Every island on the recipe page uses client:load. The page feels slow to respond on a cheap phone. What is the best first change?
Astro158 / 212
Module 17 · Performance and caching
Images and fonts decide LCP and CLS
src/pages/recipes/[slug].astro (part)
1---2import{ Image, Font }from'astro:assets'3const{ recipe }= Astro.props4---5<head>6<FontcssVariable="--font-body"preload/>7</head>8<Imagesrc={recipe.data.cover}alt={recipe.data.coverAlt}9width={1200}priority/>
<Image> resizes, converts to a modern format, and writes width and height, so the browser reserves the space before the file arrives (no layout shift).
priority loads the hero photo eagerly and early; use it on the one image that is the LCP, never on all of them.
<Font preload /> tells the browser about the font file early, so text doesn’t jump when it swaps in.
Find the bugInteractive · disabled in the PDF
Visitors see the recipe text jump down as the page loads. Which line causes it?
Astro
Astro159 / 212
Module 17 · Performance and caching
How CSS and HTML reach the browser
CSS is render-blocking: the browser waits for a page’s stylesheets before drawing it. Small stylesheets are cheaper inlined into the HTML (no extra request); large ones are better as separate files the browser can cache between pages.
build.inlineStylesheets
What happens
auto (the default)
Stylesheets under Vite’s inline limit (4 kB) go in a <style>; bigger ones become files
always
Every stylesheet is inlined
never
Every stylesheet is a separate file
compressHTML controls whitespace in the HTML. Since Astro 7 the default is "jsx": whitespace and line breaks around elements are removed the way React treats JSX, so keep a space you need with {" "}. true removes whitespace without changing how the page looks; false keeps it all.
QuizInteractive · disabled in the PDF
A blog with 200 posts shares one 60 kB stylesheet. Someone sets inlineStylesheets: "always". What happens as a reader moves from post to post?
A blog with 200 posts shares one 60 kB stylesheet. Someone sets inlineStylesheets: "always". What happens as a reader moves from post to post?
Astro160 / 212
Module 17 · Performance and caching
Prefetch the page they are about to open
Prefetching downloads a page before the click, so it opens instantly. With prefetch: true in the config, Astro adds a small script and prefetches links marked data-astro-prefetch. prefetchAll: true marks every link; defaultStrategy sets when (default hover).
data-astro-prefetch="…"
Prefetches when
hover (default)
The pointer is over the link, or it has focus
tap
Just before the click
viewport
The link scrolls into view
load
Right after the page loads
Fill the blanksInteractive · disabled in the PDF
Prefetch the next recipe as soon as its link is visible.
Astro
1<ahref={next.url}data-astro-="">Next recipe</a>
Astro161 / 212
Module 17 · Performance and caching
Caching: keep a copy so you don’t make it again
A cache keeps a copy of a response to answer the next request without redoing the work. The browser has one, and so does a CDN (content delivery network): servers around the world that keep copies close to visitors. The Cache-Control header on a response says how long copies may be kept.
File
Its address…
Cache it
/_astro/Search.Bx7f2.js
Has a hash of the content: a new build that changes it gives a new name
max-age=31536000, immutable (a year)
/recipes/lemon-cake/
Never changes, even when the recipe does
Briefly, or check for a newer copy every time
`max-age`
How many seconds a copy counts as fresh.
`stale-while-revalidate` (SWR)
For how long after that a stale copy may still be served while a fresh one is made in the background.
`immutable`
This file will never change at this address: don’t even check.
Route caching (new in Astro 7) stores responses of on-demand routes, so the next visitor to the same address gets the saved copy. A provider decides where copies live: memoryCache() keeps them in the server’s memory, and an adapter can set up its platform’s cache instead. routeRules sets rules by pattern; Astro.cache.set() (or context.cache in endpoints and middleware) sets them per response. Prerendered pages are files already, so this is for the routes that run per request. In astro dev, nothing is cached.
QuizInteractive · disabled in the PDF
With maxAge: 300, swr: 60, a visitor asks for a recipe 320 seconds after it was cached. What do they get?
With maxAge: 300, swr: 60, a visitor asks for a recipe 320 seconds after it was cached. What do they get?
Astro163 / 212
Module 17 · Performance and caching
Purge by tag when content changes, and never cache what’s personal
A tag names a group of cached responses. When a recipe changes, purge its tag. Check who is calling first (module 14).
A cached page is served to the next visitor. If a response depends on who asks (their cookie, their saved recipes, their cart), a shared cache would hand one person’s page to another. Call Astro.cache.set(false) there, or keep the personal part in an island that fetches it in the browser.
Find the bugInteractive · disabled in the PDF
This page leaks one reader’s saved recipes to others. Which line?
Astro
Astro164 / 212
Module 17 · Performance and caching
Faster builds for big sites
A blog with ten thousand recipes takes a while to prerender. Astro 7 has experimental flags for that: they may change between versions, so read the release notes before relying on them.
Flag
Since
What it does
experimental.incrementalBuild
7.2
Skips pages whose cacheKey (returned from getStaticPaths) and dependencies didn’t change
experimental.parallelPrerender
7.4
Renders static pages on several CPU cores
experimental.collectionStorage
7.1
Splits the content data store into chunks, for platforms with file-size limits
Where the time goes. Profile before you reach for flags: slow builds are usually a slow data source fetched once per page (fetch it once and share it), or many large images processed again (keep the cache folder between CI runs; Astro keeps processed images in node_modules/.astro by default, set by cacheDir).
Try itInteractive · disabled in the PDF
Astro165 / 212
Module 17 · Performance and caching
Recap: performance and caching
Measure the build with Core Web Vitals: LCP, CLS and INP.
Send less JavaScript: fewer islands, later directives.
<Image> sizes prevent layout shift; priority only for the hero image.
Hashed /_astro/ files cache for a year; HTML doesn’t.
Purge with tags when content changes; never cache personal responses.
FlashcardsInteractive · disabled in the PDF
LCP
Largest Contentful Paint: when the biggest visible element is drawn.
CLS
Cumulative Layout Shift: how much the page jumps while loading.
INP
Interaction to Next Paint: how quickly the page responds to input.
CDN
Servers around the world that keep copies of responses close to visitors.
stale-while-revalidate
Serve a stale copy for a while as a fresh one is made.
build.inlineStylesheets
auto, always or never: whether CSS goes inline or in files.
data-astro-prefetch
Marks a link to prefetch: hover, tap, viewport or load.
Astro.cache.set()
Sets cache rules for this on-demand response, or false to opt out.
Cache tag
A label on cached responses, used to purge them together.
Module 18: Security and the Content Security Policy
Astro166 / 212
Module 18 · Security and the Content Security Policy
What can go wrong on a small blog
Even a recipe blog has things worth attacking: readers’ browsers, a newsletter list, a comments form, the keys to a mail service. Four attacks cover most of what you will meet.
XSS (cross-site scripting)
An attacker gets their script into your page (through a comment, say), and it runs with your page’s powers in every reader’s browser.
CSRF (cross-site request forgery)
Another site makes the reader’s browser send a request to yours, such as a form post, with the reader’s cookies attached.
Clickjacking
Another site shows your page in an invisible frame and tricks the reader into clicking on it.
Leaked secret
A private key (for an email service, say) ends up in code sent to the browser.
Defence in depth. No single setting stops everything. Each layer in this module catches what the one before missed: escaping, then secrets kept on the server, then origin checks, then a policy the browser enforces.
Astro167 / 212
Module 18 · Security and the Content Security Policy
Expressions are escaped; set:html is not
Escaped (safe)
<p>{comment.body}</p>
Raw HTML (dangerous)
<pset:html={comment.body}/>
Escaping turns characters that mean something in HTML (<, >, &, quotes) into harmless codes, so text stays text. Astro escapes every {expression} for you. set:html skips that on purpose: keep it for HTML you made yourself, like rendered Markdown from your own files, or HTML passed through a sanitiser library first.
Find the bugInteractive · disabled in the PDF
Readers can post comments. Which line lets one reader run a script on everyone else’s screen?
Astro
Astro168 / 212
Module 18 · Security and the Content Security Policy
Recap from module 14: with astro:env, a server + secret variable can only be imported from astro:env/server, in code that runs on the server, and is read at run time. A client variable is public by definition: it is written into the JavaScript the browser downloads. Whatever reaches the browser, anyone can read with “View source”.
QuizInteractive · disabled in the PDF
Which of these puts the mail key where readers can see it?
Which of these puts the mail key where readers can see it?
Astro169 / 212
Module 18 · Security and the Content Security Policy
Astro checks where form posts come from
security.checkOrigin (on by default) protects on-demand routes from CSRF. For a POST, PUT, PATCH or DELETE that a plain HTML form could send, Astro compares where the request came from (the Sec-Fetch-Site header, or Origin) with your site. A request from another site gets a 403 Forbidden and the page never runs.
Setting
Protects against
security.checkOrigin (default true)
Form posts sent from other sites (CSRF)
security.allowedDomains
Faked X-Forwarded-Host headers changing Astro.url; unset, that header is ignored
security.actionBodySizeLimit (1 MB)
Huge request bodies sent to actions to tire the server
QuizInteractive · disabled in the PDF
Does checkOrigin protect a prerendered page?
Does checkOrigin protect a prerendered page?
Astro170 / 212
Module 18 · Security and the Content Security Policy
A Content Security Policy says what a page may load
A Content Security Policy (CSP) is a list of rules the browser enforces for a page: which scripts may run, which styles apply, where it may fetch from, where forms may post. If an attacker slips a script in anyway, the browser refuses to run it because the policy doesn’t allow it. Each rule is a directive, like script-src or img-src.
Nonces
A random value, new for every response
Needs a server rendering every response
The HTML can’t be a cached file
Hashes (what Astro does)
The SHA-256 fingerprint of each allowed script or style
Worked out once, at build time
Works for prerendered pages served as files
Directive
One rule in a CSP: img-src 'self' says images may only come from your site.
Hash
A short fingerprint of some content: change one byte and the hash changes.
`'self'`
In a directive: the page’s own origin (scheme, host and port).
Astro171 / 212
Module 18 · Security and the Content Security Policy
Astro knows every script and style it emits for a page, islands included.
It hashes each one (SHA-256 unless algorithm says SHA-384 or SHA-512).
It writes script-src and style-src with those hashes into a <meta> tag.
The browser refuses any script or style whose hash isn’t listed.
The policy is written into each page at build time.
Not under astro dev. The dev server serves modules one at a time and can’t apply the policy. Test it with astro build and astro preview, in a real browser, with the console open.
QuizInteractive · disabled in the PDF
Why does a hash-based policy suit a prerendered blog better than nonces?
Why does a hash-based policy suit a prerendered blog better than nonces?
Astro172 / 212
Module 18 · Security and the Content Security Policy
The script text lives in one constant, rendered and hashed, so the two can’t drift apart.
An is:inline script is left exactly as written (module 7), so Astro doesn’t hash it. Astro.csp adds to the policy of the page being rendered: insertScriptHash, insertStyleHash, insertScriptResource, insertStyleResource and insertDirective. For something every page needs, like a style a library injects at run time, list its hash once in scriptDirective.hashes or styleDirective.hashes.
Fill the blanksInteractive · disabled in the PDF
Hash the inline script and add it to this page’s policy.
scriptDirective.strictDynamic: true adds 'strict-dynamic': scripts the policy trusts may load further scripts. Because browsers then ignore 'self', Astro hashes every bundled script and writes an import map with their integrity, which needs recent browsers (Chrome 127, Firefox 138, Safari 18).
Astro174 / 212
Module 18 · Security and the Content Security Policy
What a meta policy can’t do, and what it rules out
Browsers ignore some directives inside a <meta>: frame-ancestors (who may frame the page, the clickjacking guard), report-uri, report-to and sandbox. Send those as HTTP headers: many static hosts read a _headers file from public/, and on-demand routes can set them in middleware.
When a header and a <meta> policy both apply, a resource must pass both.
Under Astro’s CSP
Because
<ClientRouter /> isn’t supported
Use native view transitions (module 16)
Shiki code highlighting isn’t supported
It writes inline style attributes; the docs suggest Prism
'unsafe-inline' turns hashes off
Astro stops emitting hashes for that directive, and it is weak
External scripts and styles
Not hashed for you: add their hashes or resources yourself
Find the bugInteractive · disabled in the PDF
This config is meant to stop other sites framing the blog. Which line does nothing?
JavaScript
Try itInteractive · disabled in the PDF
Astro175 / 212
Module 18 · Security and the Content Security Policy
Recap: security
{expressions} are escaped; set:html is raw: only for HTML you trust.
Secrets are server + secret in astro:env; never props of an island.
checkOrigin blocks cross-site form posts on on-demand routes.
security.csp hashes every script and style Astro builds into a <meta> policy.
Add other directives yourself, and hashes for scripts Astro can’t see.
frame-ancestors and reporting need real headers; test the policy on a build.
FlashcardsInteractive · disabled in the PDF
XSS
An attacker’s script running inside your page, in readers’ browsers.
CSRF
Another site making the reader’s browser send a request to yours.
Escaping
Turning <, >, & and quotes into codes so text can’t become markup.
CSP
Rules the browser enforces about what a page may load and run.
security.checkOrigin
Rejects cross-site unsafe requests to on-demand routes with 403.
Astro.csp.insertScriptHash()
Adds a script hash to this page’s policy.
styleDirective.hashes
Extra style hashes for every page, for styles Astro can’t see.
frame-ancestors
Who may frame the page; ignored in <meta>, so send it as a header.
Module 19: Integrations, the build pipeline and architecture
Astro176 / 212
Module 19 · Integrations, the build pipeline and architecture
An integration plugs a feature into Astro
You have already used integrations: @astrojs/react lets Astro render React islands, and an adapter (module 13) is an integration too. An integration is an object with a name and hooks: functions Astro calls at fixed moments, while it reads the config, runs the dev server, or builds.
Terminal
1npxastroaddsitemap
Installs @astrojs/sitemap and adds it to integrations for you.
A package (or object) that adds to Astro through hooks.
Hook
A function Astro calls at a named moment, like astro:build:done.
Renderer
The part of a framework integration that renders its components (React, Vue…).
Astro177 / 212
Module 19 · Integrations, the build pipeline and architecture
What astro build does, in order
Astro reads the config and runs each integration’s setup hooks.
Content loaders run and types are generated (what astro sync does alone).
Vite bundles client scripts and CSS, naming each file by a hash of its content.
Every prerendered route runs once, getStaticPaths included, to make HTML.
Files are written to dist/; an adapter adds the server for on-demand routes.
A static build; with an adapter, the server part is written too.
Put in orderInteractive · disabled in the PDF
Put these build steps in order.
1.Static pages are rendered to HTML
2.Client JavaScript and CSS are bundled with hashed names
3.Integrations’ astro:config:setup hooks run
4.astro:build:done runs with the list of pages
5.Content collections are loaded and types generated
Astro178 / 212
Module 19 · Integrations, the build pipeline and architecture
Astro is built on Vite
Vite is the tool under Astro that serves files in development and bundles them for production (Astro 7 uses Vite 8). Anything a Vite plugin can do, Astro can: pass plugins and options under the vite key. Many integrations are a thin layer that adds a Vite plugin for you.
Know which command you are in.astro:config:setup receives command (dev, build, preview or sync) and isRestart. Skip expensive work in dev, or do it only once.
Astro180 / 212
Module 19 · Integrations, the build pipeline and architecture
Module 19 · Integrations, the build pipeline and architecture
Testing components with the container API
src/components/RecipeCard.test.ts
1import{ experimental_AstroContainer as AstroContainer }2from'astro/container'3import{ expect, test }from'vitest'4import RecipeCard from'./RecipeCard.astro'56test('shows the servings',async()=>{7const container =await AstroContainer.create()8const html =await container.renderToString(RecipeCard,{9props:{title:'Lemon cake',servings:8}10})11expect(html).toContain('Serves 8')12})
The container API renders an .astro component to a string outside a site, so a test runner such as Vitest can check its HTML. renderToString also takes slots, params and locals. Point Vitest at Astro’s Vite setup with getViteConfig from astro/config in vitest.config.ts. The name starts with experimental_: its shape may still change.
QuizInteractive · disabled in the PDF
What is the container API good for?
What is the container API good for?
Astro182 / 212
Module 19 · Integrations, the build pipeline and architecture
Architecture: decide where each part runs
Part of the blog
Runs
Why
Posts, recipes, tag pages, about
Build time (static)
Same for everyone; changes when Sam publishes
Search box
Browser (island)
Responds to each key press
Newsletter sign-up
Server, on demand (action)
Needs a secret and must check input
“Saved recipes” for signed-in readers
Server island or on-demand route
Different per reader, never cached shared
Recipe of the day
On demand, cached with maxAge
Changes daily; the same for everyone
The rule of thumb: static first, an island where something moves, a server only where a request must be answered personally or freshly. Each step down that list costs more (JavaScript, servers, caching rules to get right), so take it only for a reason you can name.
QuizInteractive · disabled in the PDF
Sam adds comments to recipes. New comments should appear within a minute. Which design fits best?
Sam adds comments to recipes. New comments should appear within a minute. Which design fits best?
Astro183 / 212
Module 19 · Integrations, the build pipeline and architecture
Where the content comes from
Build-time collections
src/content.config.ts with a loader: files, an API, a CMS
Fetched when the site is built (or astro dev starts)
Fast pages; new content needs a rebuild
The usual choice for a blog
Live collections
src/live.config.ts, read with getLiveCollection and getLiveEntry
Fetched at request time, in on-demand routes
Always fresh; each view costs a request
For stock, prices, anything that changes by the minute
Moving from Markdown files to a CMS (a content management system: a web app where writers edit posts) means swapping the loader, not the pages: the collection’s schema stays the contract the pages rely on. A webhook from the CMS (a request it sends when content changes) can start a rebuild, or purge cache tags for on-demand pages (module 17).
Keep the seams clean. Put platform-specific code (bindings, a host’s APIs) behind one small module, data access behind loaders or one lib/ file, and keep pages about markup. Then changing host, CMS or framework touches one place. In a monorepo, share components as a package and let each site keep its own config.
Try itInteractive · disabled in the PDF
Astro184 / 212
Module 19 · Integrations, the build pipeline and architecture
Recap: integrations and architecture
Integrations are objects with a name and hooks; adapters and renderers are integrations.
astro build: config, content sync, Vite bundles, prerendering, dist/, then the adapter’s server.
Astro runs on Vite: plugins go under vite; astro check type-checks in CI.
astro:config:setup changes the site; astro:build:done works on the output.
The container API renders a component in a test.
Static first, islands where it moves, the server only where a request needs it.
FlashcardsInteractive · disabled in the PDF
Integration
An object with a name and hooks that extends Astro.
astro add
Installs an integration and adds it to the config.
injectRoute()
Adds a route from an integration: a pattern and an entrypoint.
astro:build:done
The last build hook: gets pages, dir, assets and logger.
Vite
The dev server and bundler Astro is built on.
astro check
Type-checks the project, .astro files included.
Container API
experimental_AstroContainer: renders components to strings, for tests.
Live collection
Content fetched at request time, from src/live.config.ts.
Webhook
A request one service sends another when something happens.
Module 20: Mastery challenge
Astro185 / 212
Module 20 · Mastery challenge
How this challenge works
Each slide is a situation from Sam’s blog. There are no new ideas here: every answer is somewhere in the course. Before you pick, say out loud where the code runs (build, server per request, or browser) and when. Most mistakes in Astro come from getting that wrong.
Predict the outputInteractive · disabled in the PDF
In a prerendered recipe page, which of these run in the reader’s browser?
Astro
1---2const recipe =awaitgetEntry('recipes','lemon-cake')// A3---4<h1>{recipe.data.title}</h1>5<Searchclient:idle/><!-- B -->6<Comments/><!-- C -->7<script>8 console.log('ready')// D9</script>
In a prerendered recipe page, which of these run in the reader’s browser?
Astro186 / 212
Module 20 · Mastery challenge
Review: a dynamic route
Find the bugInteractive · disabled in the PDF
The build stops on the first recipe page. Which line is wrong?
Astro
QuizInteractive · disabled in the PDF
Sam adds src/content/recipes/plum-tart.md to the deployed, fully static site. When does /recipes/plum-tart/ exist?
Sam adds src/content/recipes/plum-tart.md to the deployed, fully static site. When does /recipes/plum-tart/ exist?
Astro187 / 212
Module 20 · Mastery challenge
Predict: an island’s props
Predict the outputInteractive · disabled in the PDF
The site has the default output: "static" and an adapter. Which routes should export prerender = false?
The site has the default output: "static" and an adapter. Which routes should export prerender = false?
Put in orderInteractive · disabled in the PDF
A reader signs up for the newsletter on an on-demand page. Put the steps in order.
1.The page renders with Astro.getActionResult()
2.The browser posts the form to the page
3.Middleware runs and fills locals
4.Astro checks the request’s origin (checkOrigin)
5.The action validates the input and runs its handler
Astro189 / 212
Module 20 · Mastery challenge
Complete: middleware for the members area
Fill the blanksInteractive · disabled in the PDF
Send signed-out readers away from /members/, and let everyone else through.
TypeScript
1import{}from'astro:middleware'23exportconst onRequest =defineMiddleware(async(context, next)=>{4const user =awaitreadUser(context.cookies)5 context..user = user6if(context.url.pathname.startsWith('/members/')&&!user){7return context.('/sign-in/')8}9return()10})
QuizInteractive · disabled in the PDF
The middleware above is in place, but /members/ was left prerendered. What does a signed-out reader get in production?
The middleware above is in place, but /members/ was left prerendered. What does a signed-out reader get in production?
Astro190 / 212
Module 20 · Mastery challenge
Find the bug: the newsletter action
Find the bugInteractive · disabled in the PDF
The form posts with method="POST" action={actions.subscribe}, and every submission comes back with an error. Which line?
TypeScript
Astro191 / 212
Module 20 · Mastery challenge
Review a pull request
A contributor opens a pull request that adds a “Reader tips” section to recipes. Read the summary of changes and decide which review comments are right.
Tips are posted by readers and shown with <div set:html={tip.body} />.
The tips island uses client:load, at the bottom of the page.
The recipe route is now on demand and calls Astro.cache.set({ maxAge: 600 }).
The page also shows “Your saved tips” for the signed-in reader.
"frame-ancestors 'none'" was added to security.csp.directives.
QuizInteractive · disabled in the PDF
Which review comments are right?
Which review comments are right?
Astro192 / 212
Module 20 · Mastery challenge
Design: a recipe site that grows
The blog is popular. Sam now wants: Spanish versions of every recipe, a weekly “what’s in season” box that updates without a rebuild, page-to-page animations, and a strict CSP. You are asked for one plan.
QuizInteractive · disabled in the PDF
Which plan fits all four wishes?
Which plan fits all four wishes?
Your final projectInteractive · disabled in the PDF
Astro193 / 212
Module 20 · Mastery challenge
Recap: thinking like an Astro expert
First ask where and when code runs: build, server per request, or browser.
Static first; islands where something moves; a server only for a named reason.
Props cross into islands as data, in the page source: keep them small and public.
Anything personal must not be prerendered or shared-cached.
Protection (middleware, origin checks) only runs for on-demand requests.
Defaults are safe (escaping, checkOrigin); set:html and 'unsafe-inline' are where you opt out.
FlashcardsInteractive · disabled in the PDF
Where does frontmatter run?
On the server: once at build time for prerendered pages, per request for on-demand ones.
A framework component with no directive
Rendered to HTML; sends no JavaScript.
What can’t an island prop be?
A function, or anything else that can’t be written into the page as data.
Default accept of an action
json; HTML forms need accept: "form".
Middleware on a prerendered page
Runs once at build time; never for later requests to that file.
Personal content and caching
Opt out with Astro.cache.set(false), or move it into an island.
Animations under a strict CSP
Native cross-document view transitions in CSS.
Module 21: Final project: a conference website
Astro194 / 212
Module 21 · Final project: a conference website
Build a conference website on your own computer
You will build the website of Harbour Web Day, a one-day conference, with Astro 7 and its real tools: astro dev, astro check and astro build. It has a schedule, a page for every talk and every tag, and a page for wrong addresses. The talks are Markdown files, and a small script filters the schedule as you type. On the way you practise:
Layouts, named slots and Astro.url (modules 6 and 9).
Components with typed props, scoped styles and a <script> (modules 5 and 7).
A 404 page, and dynamic routes made with getStaticPaths (module 8).
A content collection with a schema, and render (module 10).
An endpoint that becomes a file, a check, a build and a preview (modules 3, 13 and 14).
You need Node.js 22.23.4 or newer (node --version prints yours; Astro 7.4 refuses to run on an older one), a terminal and a code editor. Download the starter and unzip it: it makes a folder, astro-project/.
astro-project/
src/
content/
talks/# the talks, one Markdown file each
closing-keynote.md
…# eight in all
pages/# the three pages so far
about.astro
index.astro
schedule.astro
styles/
global.css# the site’s look, done for you
.gitignore
astro.config.mjs
package.json# astro 7.4.10, and the checker
README.md
tsconfig.json
Terminal
1cdastro-project2npminstall# once: downloads Astro and its tools3npmrundev# starts the dev server
Text
1> astro dev23…4 astro v7.4.10 ready in 86 ms5┃ Local http://localhost:4321/6┃ Network use --host to expose716:01:05 watching for file changes...
What npm run dev prints, give or take the times.
Open http://localhost:4321/: the home page, with Home, Schedule and About in the header. Every tab says just “Harbour Web Day”, the nav doesn’t show where you are, and the schedule says “The talks will be listed here.” Keep the dev server running while you work: the page updates each time you save. Ctrl + C stops it.
Get it runningInteractive · disabled in the PDF
Astro195 / 212
Module 21 · Final project: a conference website
Step 1: one layout for every page
The three pages repeat the same frame: the <head>, the header, the footer. To change the nav, you would change it three times. Goal: move the frame into a layout, so each page keeps only its own content and gives its own title. Layouts are module 9; components and interface Props, module 5. Start by cutting the <header> out of index.astro into a component:
src/components/Header.astro
1<headerclass="site-header">2<spanclass="brand">Harbour Web Day</span>3<nav>4<ahref="/">Home</a>5<ahref="/schedule/">Schedule</a>6<ahref="/about/">About</a>7</nav>8</header>
src/layouts/BaseLayout.astro
1---2import Header from'../components/Header.astro'3import'../styles/global.css'45interfaceProps{ title:string; description?:string}67const about ='A one-day conference about the web, in Bristol'8const{ title, description = about }= Astro.props9---1011<!doctypehtml>12<htmllang="en">13<head>14<metacharset="utf-8"/>15<metaname="viewport"content="width=device-width"/>16<title>{title} · Harbour Web Day</title>17<metaname="description"content={description}/>18</head>19<body>20<Header/>21<divclass="page">22<main><slot/></main>23</div>24<footerclass="site-footer">25 Harbour Web Day · Friday 12 March 2027 · Harbour Hall, Bristol26</footer>27</body>28</html>
A page passes a title, and may pass a description; without one, it gets the site’s. The page itself goes in the <slot />.
Astro196 / 212
Module 21 · Final project: a conference website
Step 1: each page keeps only its own content
In each page, keep what’s inside <main>, delete everything around it (the stylesheet’s import too: the layout has it), and wrap what’s left in <BaseLayout>:
src/pages/schedule.astro
1---2import BaseLayout from'../layouts/BaseLayout.astro'3---45<BaseLayout6title="Schedule"7description="Every talk at Harbour Web Day, earliest first"8>9<h1>Schedule</h1>10<pclass="lead">Friday 12 March 2027. Doors open at 09:00.</p>11<p>The talks will be listed here.</p>12</BaseLayout>
Do the same in index.astro, as <BaseLayout title="Home">, and in about.astro, as <BaseLayout title="About" description="Who we are and how to reach us">.
Look: the pages look as they did, which is the point. The tabs now say Home · Harbour Web Day, Schedule · Harbour Web Day and About · Harbour Web Day, and each page’s source (Ctrl + U in most browsers) has its own <meta name="description">.
Step 1Interactive · disabled in the PDF
Astro197 / 212
Module 21 · Final project: a conference website
Step 2: mark the current page
A visitor should see where they are. Goal: the nav link to the page being shown gets aria-current="page". Screen readers announce it, and the starter’s stylesheet already underlines it. Astro.url is the address being rendered (module 9). Make the links from an array, so the rule is written once:
src/components/Header.astro
1---2const links =[3{href:'/',label:'Home'},4{href:'/schedule/',label:'Schedule'},5{href:'/about/',label:'About'}6]78// /about and /about/ are the same page: drop the end slash.9consttrim=(path:string)=> path.replace(/\/$/,'')||'/'10const here =trim(Astro.url.pathname)11---1213<headerclass="site-header">14<spanclass="brand">Harbour Web Day</span>15<nav>16{links.map(({ href, label })=>(17<a18href={href}19aria-current={trim(href)=== here ?'page':undefined}20>{label}</a>21))}22</nav>23</header>
The starter’s config has trailingSlash: 'ignore', so /about and /about/ both show the about page; trim makes them compare equal, and keeps / as /. An attribute set to undefined isn’t written at all. Look: About is underlined at /about and at /about/, Schedule on the schedule, and Home only on the home page.
QuizInteractive · disabled in the PDF
So that a page inside a section marks it too, you compare with here.startsWith(trim(href)) instead of ===. What does the nav show on /about/?
So that a page inside a section marks it too, you compare with here.startsWith(trim(href)) instead of ===. What does the nav show on /about/?
Step 2Interactive · disabled in the PDF
Astro198 / 212
Module 21 · Final project: a conference website
Step 3: a page for wrong addresses
An address that doesn’t exist gets Astro’s plain 404 page. Goal: the site’s own, in the layout, with a link home, and asking search engines not to list it. src/pages/404.astro is module 8; a named slot that lets a page add to the <head> is module 9. In the layout, put the slot at the end of <head>:
1---2import BaseLayout from'../layouts/BaseLayout.astro'3---45<BaseLayouttitle="Page not found">6<metaslot="head"name="robots"content="noindex"/>7<h1>Page not found</h1>8<p>9 There’s no page at this address. It may have moved, or the10 link may be wrong.11</p>12<p><ahref="/">Back to the home page</a></p>13</BaseLayout>
Look: open http://localhost:4321/nowhere. It’s your page, with no nav link marked, and the terminal logs [404] /nowhere. Its source has <meta name="robots" content="noindex">; the other pages leave the slot empty, so they don’t.
Step 3Interactive · disabled in the PDF
Astro199 / 212
Module 21 · Final project: a conference website
Step 4: the talks become a content collection
The schedule should list the talks. They’re in src/content/talks/, a Markdown file each: the frontmatter holds the title, the speaker, the start time, the length, the room, the tags and a one-line summary, and the body is the abstract. Goal: declare them as a content collection with a schema (module 10), so every file is checked and every page gets typed data.
Each talk’s id is its file’s name: opening-keynote.md is opening-keynote.
A dev server that’s already running doesn’t pick up a new content.config.ts: when a page asks for the talks, the terminal warns that the collection “does not exist or is empty”. Restart it (Ctrl + C, then npm run dev), and it syncs the collection as it starts:
Text
115:56:01 [content] Syncing content215:56:01 [content] Synced content3 astro v7.4.10 ready in 357 ms
Astro200 / 212
Module 21 · Final project: a conference website
Step 4: the schema checks every file
src/content/talks/opening-keynote.md (shortened)
1---2title: 'The web we want to keep'3speaker:4 name: Amara Okafor5 role: Web standards advocate6 bio: Amara has worked on browsers, on standards and …7start: '09:30'8minutes: 459room: Main hall10keynote: true11summary: 'Links that last, pages that load on a train, …'12---1314The day opens with what makes the web worth building for: …
Each rule in the schema is something a talk must have. This one leaves out tags, so it gets []; the talks that aren’t keynotes leave out keynote, so they get false. Try breaking it: write start: 9:30 and save. The page keeps the last good data, and the terminal says that talks → opening-keynote doesn’t match the collection’s schema, and why (astro build would stop there):
Text
115:56:24 [ERROR] [glob-loader] Failed to reload opening-keynote.md: …23 **start**: Invalid string: must match pattern /^\d\d:\d\d$/
QuizInteractive · disabled in the PDF
Without .regex(…), the file with start: 9:30 would load. Where would the opening keynote land on a schedule sorted by start?
Without .regex(…), the file with start: 9:30 would load. Where would the opening keynote land on a schedule sorted by start?
Astro201 / 212
Module 21 · Final project: a conference website
Step 4: a card for each talk
On the schedule, each talk is a card: a component of its own (module 5) that takes one talk. CollectionEntry<'talks'> is the type of one entry, made from your schema: an id, and the checked frontmatter as data.
class:list adds keynote when data.keynote is true. Under the markup, the card’s style. A <style> in a component is scoped (module 7): these h2 and p rules reach only the card’s own.
getCollection doesn’t promise an order (module 10), so sort the talks yourself. The helper goes in a file of its own, because the tag pages will need it too, and it names the entry’s type Talk for the other files:
src/lib/talks.ts
1importtype{ CollectionEntry }from'astro:content'23/** One talk: its id, and its data checked by the schema. */4exporttypeTalk=CollectionEntry<'talks'>56/** Earliest first ("09:30" before "10:30"), then by room. */7exportfunctionbyStart(talks:Talk[]):Talk[]{8return talks.toSorted((a, b)=>9 a.data.start.localeCompare(b.data.start)||10 a.data.room.localeCompare(b.data.room)11)12}
In schedule.astro, get the talks, sort them, and put a card for each where the placeholder paragraph was:
src/pages/schedule.astro
1---2import{ getCollection }from'astro:content'3import BaseLayout from'../layouts/BaseLayout.astro'4import TalkCard from'../components/TalkCard.astro'5import{ byStart }from'../lib/talks'67const talks =byStart(awaitgetCollection('talks'))8---910<BaseLayout11title="Schedule"12description="Every talk at Harbour Web Day, earliest first"13>14<h1>Schedule</h1>15<pclass="lead">Friday 12 March 2027. Doors open at 09:00.</p>16<divclass="cards">17{talks.map((talk)=><TalkCardtalk={talk}/>)}18</div>19</BaseLayout>
Look: the schedule lists the eight talks, from the opening keynote at 09:30 to the closing one at 16:00, with both keynotes tinted. Talks that start together are in room order: Main hall, then Room 2.
Step 4Interactive · disabled in the PDF
Astro203 / 212
Module 21 · Final project: a conference website
Step 5: a page for each talk
Each card links to /talks/<id>/, which doesn’t exist yet. Goal: a page per talk, from one file. Its getStaticPaths (module 8) lists a page per entry and hands the entry to it, and render turns the Markdown body into a Content component (module 10).
Look: click a talk on the schedule. Its page has the time, the room, the summary and the abstract, and its title in the tab and in search results.
Astro204 / 212
Module 21 · Final project: a conference website
Step 5: only the listed pages exist
Open http://localhost:4321/talks/nope/: your 404 page. getStaticPaths didn’t list nope, so there’s no such page, and the dev server warns that “no matching static path was found for requested path /talks/nope/”:
Text
115:57:22 [WARN] [router] A `getStaticPaths()` route pattern was …23Possible dynamic routes being matched: src/pages/talks/[id].astro.415:57:22 [404] /talks/nope/ 5ms
Find the bugInteractive · disabled in the PDF
This comes from a tutorial for an older Astro. In Astro 7, every talk page fails with Missing parameter: id. Which line is wrong?
Astro
Step 5Interactive · disabled in the PDF
Astro205 / 212
Module 21 · Final project: a conference website
Step 6: put the speaker beside the talk
Goal: a talk’s page shows who gives it, beside the talk on a wide screen. Give the layout a second named slot, aside: a page that fills it gets an <aside> next to <main>, and a page that doesn’t gets none, rather than an empty box. Astro.slots.has and <Fragment slot> are module 6.
The starter’s stylesheet makes .page.with-aside two columns on a wide screen.
On the talk’s page, fill the slot after <Content />, inside <BaseLayout>. A slot attribute goes on one element, so a Fragment carries the three together and leaves no tag of its own:
Look: a talk’s page has the speaker’s name, role and bio in a box beside the talk, or under it on a phone. The other pages have no <aside> at all.
Step 6Interactive · disabled in the PDF
Astro206 / 212
Module 21 · Final project: a conference website
Step 7: a page for each tag
Goal: /tags/css/ lists the talks tagged css, earliest first, under a count: “3 talks”, or “1 talk”. The tags aren’t a list of their own: collect them from the talks, each once, with a Set (as module 10’s tag pages do).
src/pages/tags/[tag].astro
1---2import{ getCollection }from'astro:content'3import BaseLayout from'../../layouts/BaseLayout.astro'4import TalkCard from'../../components/TalkCard.astro'5import{ byStart,type Talk }from'../../lib/talks'67exportasyncfunctiongetStaticPaths(){8const talks =byStart(awaitgetCollection('talks'))9const tags =newSet(talks.flatMap((talk)=> talk.data.tags))10return[...tags].map((tag)=>({11params:{ tag },12props:{tagged: talks.filter((t)=> t.data.tags.includes(tag))}13}))14}1516interfaceProps{ tagged:Talk[]}1718const{ tag }= Astro.params19const{ tagged }= Astro.props20const title =`Talks tagged ${tag}`21---
Look: /tags/css/ has three talks, /tags/javascript/ says “1 talk”, and /tags/php/ is your 404 page.
Astro207 / 212
Module 21 · Final project: a conference website
Step 7: link each talk to its tags
On a talk’s page, list its tags as links, under the summary. The opening keynote has none, so write the list only when there’s something in it, rather than an empty <ul>:
Look: “Colour that passes contrast” has three tags, and css opens /tags/css/. The opening keynote has no list.
QuizInteractive · disabled in the PDF
Eight talks have 13 tags between them, and three of them have css. How many pages does src/pages/tags/[tag].astro make?
Eight talks have 13 tags between them, and three of them have css. How many pages does src/pages/tags/[tag].astro make?
Step 7Interactive · disabled in the PDF
Astro208 / 212
Module 21 · Final project: a conference website
Step 8: filter the schedule as you type
So far the site sends no JavaScript at all (module 12). Goal: one small exception, a box on the schedule that hides the talks that don’t match what you type. A <script> in a component is enough (module 7): Astro bundles it, TypeScript and all, into the pages that use the component.
In schedule.astro, import it and put <TalkFilter /> just above <div class="cards">. The box starts hidden, and the script shows it: without JavaScript, there’s no box that does nothing, only the whole schedule. Look: type room 2 and three talks are left; priya, one; php, “No talks match.”
Step 8Interactive · disabled in the PDF
Going deeper
hidden works through the browser’s own rule, [hidden] { display: none }, and any display in your CSS beats it. If you give .card or .filter a display: flex, hidden stops hiding them: add [hidden] { display: none !important } to the stylesheet, or toggle a class instead.
Astro209 / 212
Module 21 · Final project: a conference website
Step 9: a JSON feed of the talks
Last, get the site ready to ship, starting with a route that isn’t a page: /talks.json, the schedule as data, for an app or another site to read. An endpoint (module 14) exports a GET that returns a Response. On a static site it runs once, during the build, and what it returns is saved as a file.
talks.json.ts answers at /talks.json: the .ts is dropped.
Look: http://localhost:4321/talks.json is the eight talks, earliest first: [{"title":"The web we want to keep","speaker":"Amara Okafor","start":"09:30","room":"Main hall","url":"/talks/opening-keynote/"},…
Astro210 / 212
Module 21 · Final project: a conference website
Step 9: check the types
The dev server doesn’t check types: it draws the page, right or wrong. npm run check runs astro check, which type-checks every .astro and .ts file, the templates and the scripts included, with the types your schema makes. Stop the dev server, and run it:
Eighteen pages: home, about, schedule, 404, six tags and eight talks, with talks.json beside them. The filter’s script is small, so it’s written into the schedule’s HTML; the other pages have no script at all. npm run preview serves dist/ as a host would:
Text
1> astro preview23 astro v7.4.10 ready in 18 ms4┃ Local http://localhost:4321/5┃ Network use --host to expose
Step 9Interactive · disabled in the PDF
Astro212 / 212
Module 21 · Final project: a conference website
What you built, and where to go next
A layout with named slots for the head and an aside, and a nav that knows the current page.
A schedule made from a content collection, checked by a schema and typed from it.
A page for every talk and every tag, from two dynamic routes, and a 404 for the rest.
A small script on the one page that needs it, and a JSON feed made at build time.
A site that passes astro check, and builds to plain files in dist/.
Deploy it (module 13). dist/ is a folder of static files, so any static host can serve it: upload the folder, or connect the host to your Git repository and give it the build command, npm run build, and the output folder, dist. Set site in astro.config.mjs to the address it gets, so Astro can make full links.
Add a canonical link and Open Graph tags to the layout’s head, from site (module 9).
Give each speaker a photo with <Image />, from the collection (module 11).
Animate the move from the schedule to a talk with a view transition (module 16).
Take it furtherInteractive · disabled in the PDF
Answer key
1. How a website reaches you · An address says which server, and which page · QuizIn https://example.com/blog/first-post?ref=home, what is the path?B. /blog/first-postThe path starts at the first / after the domain and stops before ?. example.com is the domain (which server), and ?ref=home is the query (extra details). The path is what tells the server which page you want.
1. How a website reaches you · The browser reads the HTML, then fetches what it points to · Put in orderPut a first visit to a page in order.1. You type an address and press Enter 2. The browser sends a request to the server 3. The server answers with HTML 4. The browser asks for the CSS and images the HTML links to 5. The page appears on screenNothing can be drawn before the HTML arrives, because the HTML is what tells the browser which other files exist. That is why a page with small, ready-made HTML appears quickly.
1. How a website reaches you · Static pages are made ahead; dynamic pages are made per visit · QuizWhich page needs to be dynamic (made per visit)?C. A page that greets the signed-in reader by nameThe greeting depends on who is asking, which is only known when the request arrives. A list of posts changes only when you publish, so it can be made again then, ahead of time, and still be static.
1. How a website reaches you · Every bit of JavaScript costs the visitor time · QuizWhich parts of a recipe page need JavaScript? Pick all that apply.B. A button that doubles every quantity in place; D. A search box that filters recipes as you typeThe ingredients and the link work with HTML alone: a link is just an <a> element, which browsers follow without any script. Changing quantities in place and filtering as you type both react to the reader, after the page has loaded, so they need JavaScript.
2. Tools, frameworks and the build step · Check: which tool does what · QuizYou want to add a package called date-fns to your project. What do you use?B. npm, by running npm install date-fnsnpm is the tool that downloads packages and records them in package.json. Node runs JavaScript files, but node date-fns would look for a file to run, not install anything.
2. Tools, frameworks and the build step · Check: which tool does what · QuizYou open a terminal and type npm run dev, but it says it can’t find package.json. What is the likely cause?B. The terminal is in the wrong folder: cd into the project firstnpm looks for package.json in the folder the terminal is in (and the folders above it). pwd shows where you are and cd moves you. A missing internet connection would fail differently, and only when downloading.
2. Tools, frameworks and the build step · While you work, a dev server rebuilds as you save · QuizYou want to publish your blog to a hosting service. What do you give it?B. The dist/ folder written by astro buildBrowsers can’t read .astro files: the build turns them into HTML. dist/ is the finished site. The dev server runs on your own computer and stops when you close it. (Many hosts run the build for you from your source, but what they serve is still the build’s output.)
2. Tools, frameworks and the build step · The same page, two ways · QuizA reader on a slow phone opens a recipe. With which approach do they see the text sooner?B. The Astro page: the text is already in the HTML that arrives firstThe browser can draw HTML as it arrives. In a single-page app the text exists only after a script is downloaded and run, which is slowest exactly on slow phones. Same text, different route to the screen.
3. Your first Astro project · Each folder has one job · QuizYou want a new page at /contact. Where do you create it?B. src/pages/contact.astroOnly files in src/pages/ become pages, and their file name becomes the path. A file in public/ would be copied as is, without any of Astro’s help, and a file loose in src/ is not a page at all.
3. Your first Astro project · package.json lists the commands you will run · Fill the blanksFill in the commands: start writing, then make the finished site, then look at it.1: dev, 2: build, 3: previewdev is for writing: it rebuilds as you save. build writes the finished site into dist/. preview serves that dist/ folder on your computer, so you can check exactly what visitors will get.
3. Your first Astro project · A new file in src/pages is a new address · QuizYou create src/pages/recipes.astro. What address shows it?B. /recipesThe path is the file’s place inside src/pages/, without the extension. src/pages/ itself is the root of the site, so recipes.astro becomes /recipes, and index.astro becomes /.
3. Your first Astro project · Build writes the finished site into dist · QuizYou fix a typo directly in dist/index.html, then run npm run build again. What happens to the fix?B. It is lost: the build rewrites dist/ from your sourcedist/ is output: the build makes it again from src/ and public/ every time. Always fix the source (src/pages/index.astro), then build.
3. Your first Astro project · Read an error from the top: what, where, which line · Find the bugThis page fails to build with a syntax error. Which line is wrong?Line 2: const title = 'Soups'A string that opens with ' must close with one on the same line. Line 2 never closes it, and a quoted string can’t carry on into the next line, so the parser stops there. In general an error names a problem, not always its exact spot: look at the line it points to and the one just before.
4. Inside an .astro file · The script runs while the page is built, never in the browser · QuizWhere does the console.log message appear?B. In the terminal running astro dev or astro buildThe script runs in Node, on your computer, while the page is made. Its console is the terminal. The browser only ever receives the finished HTML, so builtAt is the build’s date for every visitor, not today’s date when they visit.
4. Inside an .astro file · Curly braces put values into the HTML · Fill the blanksShow the recipe’s name in the heading, and use its photo as the image source.1: {recipe.name}, 2: {recipe.photo}Braces switch from HTML to JavaScript. In text, {recipe.name} is replaced by its value. In an attribute, braces replace the quotes entirely: src="{recipe.photo}" would be the literal text {recipe.photo}.
4. Inside an .astro file · Show something only sometimes · QuizWhat does this render?A. <p>Quick</p>15 < 20 is true, so the ternary gives <p>Quick</p>. 15 > 60 is false, so && stops there and gives false, which Astro renders as nothing (not the word "false").
4. Inside an .astro file · Turn an array into a list with map · Find the bugThe page shows an empty list. Which line is wrong?Line 5: {steps.map((step) => <li>{step}</li>)}With { } after the arrow, the function has a body and returns nothing unless it says return. So map collects three undefined values, which render as nothing. Drop the braces (or write return <li>…</li>) to return the element.
4. Inside an .astro file · The script can wait for data · QuizThe tips file changes after you build the site. What do visitors see?B. The tips as they were at build time, until you build againThe fetch ran once, in the build, and its result became part of the HTML file. To pick up new data, build again (many hosts can rebuild on a schedule), or render the page per request, which module 13 covers.
5. Components and props · Import a component, then use it like a tag · QuizFrom src/pages/recipes/index.astro, which import finds src/components/Header.astro?B. import Header from '../../components/Header.astro'The page is two folders below src/ (src/pages/recipes/), so it goes up twice with ../../, reaching src/, then down into components/. One ../ only reaches src/pages/.
5. Components and props · An interface says which props a component takes · Fill the blanksType the props of a tag link: a required label and an optional count, defaulting to 0.1: Props, 2: string, 3: ?, 4: = 0, 5: Astro.propsAstro looks for an interface called exactly Props. ? marks count optional in the type, and = 0 supplies the value when it is missing. Both are needed: without the default, count would be undefined and show as nothing.
5. Components and props · Components and map make lists of cards · QuizA fourth recipe is added to the array. What else must change for it to appear?B. Nothing: map makes one card per itemThe template describes one card and map repeats it for each item, however many there are. The component and its props stay the same because each recipe has the same shape.
5. Components and props · Astro components render once, to HTML · QuizWhat happens with this Astro component when a reader clicks the button?B. Nothing useful: the script ran at build time and isn’t in the pageThe script ran once, on the server, and likes stayed 0. A function can’t be turned into HTML, so the click does nothing useful. For behaviour you add a browser <script> (module 7) or an island (module 12).
5. Components and props · Pass through the attributes you don’t name · Find the bugCallers can’t add their own aria-label: it never appears. Which line is wrong?Line 8: <a href={href} class="button" {...rest}>Read more</a>The props are typed, so the editor accepts aria-label, and line 6 collects it into rest. But line 8 never puts rest on the element, so it is dropped. Spread it on the <a> with {...rest}.
6. Slots: components that wrap other things · Check: where do the children go · QuizWhat HTML does this produce?A. <aside class="note"><strong>Note:</strong> Use cold butter.</aside>Components disappear from the output: only their HTML remains, and the children land exactly where <slot /> was. The text does not end up after the component, and it does not need a prop: anything between the tags is the default slot.
6. Slots: components that wrap other things · Practise: fill a named slot · Fill the blanksThe layout has a sidebar slot. Put the list of tags in it, and the post in the default slot.1: name, 2: slotThe component names the hole with name, the parent targets it with slot. They are different attributes on purpose: name on a <slot> declares, slot on a child chooses. The <article> has no slot attribute, so it goes to the default slot inside <main>.
6. Slots: components that wrap other things · Astro.slots.has asks if a slot was filled · Find the bugThe footer never shows, even when the page passes one. Which line is wrong?Line 2: const hasFooter = Astro.slots.has('footer')Slot names are compared exactly, and upper and lower case count. "Footer" is a different slot from "footer", which nobody fills, so has returns false every time. The template line is fine: it only runs when hasFooter is true.
6. Slots: components that wrap other things · Check: props or a slot · QuizWhich of these are better as a slot than as a prop?B. The body of a warning box, which may contain links and bold text; D. The whole content of a page, inside a page frame componentRich markup belongs in slots: it can contain any elements and components, and the parent writes it naturally. A number or a URL is a single value: a prop is clearer and can be typed in interface Props. Passing HTML as a string prop would also need set:html, which is unsafe with untrusted text.
7. Styles and small scripts · Check: what a scoped rule reaches · QuizRecipeCard.astro has <style> p { color: gray; } </style>. The page around it also has paragraphs. Which ones turn gray?B. Only the <p> elements written in RecipeCard.astro’s own templateScoping limits the rule to the elements the component itself writes. The page’s paragraphs don’t carry the card’s marker, so they are untouched. Plain element selectors are fine in scoped styles: that is the point of scoping.
7. Styles and small scripts · class:list builds a class from conditions · Fill the blanksGive the recipe card the class quick only when it takes under 30 minutes.1: class:list, 2: <class:list keeps the string card and adds quick when its value is truthy. A plain class attribute would print the array as text. In an object, the key is the class name and the value is the condition.
7. Styles and small scripts · is:inline leaves a script exactly as written · Find the bugThe page shows three CopyLink buttons, but only the first one works. Which line is the bug?Line 3: for (const button of document.querySelectorAll('.copy-link'))A processed script runs once per page, not once per component. querySelector returns only the first match, so only the first button gets a listener. Loop over querySelectorAll (or use a custom element) so every copy is set up. Making it is:inline would also “work”, but would repeat the code three times.
8. Pages, links and routes · Links are plain <a> tags · QuizYou want a page at /recipes/breakfast. Which files would answer it?A. src/pages/recipes/breakfast.astro; B. src/pages/recipes/breakfast/index.astroBoth files inside src/pages/ map to /recipes/breakfast: a file named after the address, or an index.astro in a folder of that name. Have only one of them, or Astro complains about a conflict. Files outside src/pages/ are never routes, and public/ is copied as is, not rendered.
8. Pages, links and routes · Practise: tags pages · Fill the blanksBuild one page per tag at /tags/[tag], each with the recipes that have that tag.1: getStaticPaths, 2: params, 3: params, 4: propsA Set keeps each tag once, so there is one page per tag, not one per use. The key tag in params must match the [tag] in the file name. Astro.params reads the address part; Astro.props reads what you passed. Mixing them up (Astro.props.tag) gives undefined.
8. Pages, links and routes · When two routes match, the more specific one wins · Put in orderPut these files in the order Astro tries them for /recipes/new, first to last.1. src/pages/recipes/new.astro 2. src/pages/recipes/[slug].astro 3. src/pages/recipes/[...path].astro 4. src/pages/[...all].astroA file with the exact name is most specific, so new.astro wins and [slug] never sees new. A named parameter matches exactly one piece; a rest parameter can match anything, so it comes after. A rest parameter at the top level is the last resort of all, which makes it a good catch-all.
8. Pages, links and routes · Not found pages and redirects · Find the bugThe build fails with a missing parameter error for src/pages/blog/[slug].astro. Which line is wrong?Line 5: params: { slug: post.slug },The keys in params must match the names in brackets in the file name. The file is [slug].astro, so Astro expects params.slug and finds only id. Renaming the file to [id].astro would fix it too: the name in brackets is what counts.
9. Layouts and the page head · A page wraps itself in the layout · QuizA page uses <BaseLayout title="Recipes">, but its <h1> and list never appear. What is the most likely cause?B. The layout has no <slot />Children only appear where the component places a slot (module 6). Without <slot /> in the layout, everything between <BaseLayout> and </BaseLayout> is dropped, while the frame still renders. Layouts can live anywhere and take any props.
9. Layouts and the page head · Mark the current page in the navigation · Find the bugOn /recipes/soup/, the Recipes link should be highlighted too, but it isn’t. Which line should change?Line 3: const isActive = (href: string) => path.startsWith(href)An exact comparison only matches the section’s own page. startsWith also matches pages inside it, like /recipes/soup/. Keep an exact match for the home link, though: every address starts with /. Strictly, aria-current="page" should only be on the exact page; for the section, a class is the more accurate choice.
9. Layouts and the page head · Layouts can wrap layouts · Fill the blanksMake a PostLayout that sits inside BaseLayout, passes the title on, and places the post.1: BaseLayout, 2: title, 3: slotThe inner layout uses the outer one as a component and gives it the props it needs. Its own <slot /> is where the page’s content goes; that slot is itself inside BaseLayout’s slot. Forgetting to pass title on would leave the browser tab without a page title.
10. Markdown and content collections · Zod in five minutes · QuizA recipe has servings: four in its frontmatter. With the schema from the last slide, what happens?B. The build stops with an error naming the file and the servings fieldYAML reads four as text, and z.number() rejects text, so Astro reports the bad entry when it loads the collection, in astro dev and in astro build. That is the point of a schema: a broken file fails early, loudly and at its source, not as a strange page later.
10. Markdown and content collections · Practise: list the quick recipes · Fill the blanksList the published recipes tagged quick, sorted by title.1: getCollection, 2: data, 3: includesThe filter receives each entry and keeps it when it returns true. Frontmatter lives under data, not on the entry itself: entry.tags would be undefined. includes checks whether the array has the tag; localeCompare sorts text alphabetically.
10. Markdown and content collections · One page per entry, with render · Find the bugThis page comes from an old tutorial and fails in Astro 7. Which line?Line 5: const { Content } = await render(recipe) // import renderEntries no longer have a render() method: import render from astro:content and call render(recipe). getEntry(collection, id) is current, and so is checking for undefined, since an id may not exist.
10. Markdown and content collections · Data files and references between collections · QuizWhat is post.data.author after loading the collection?B. A pointer like { collection: "authors", id: "sam" }A reference is checked (an unknown id is reported as an error when the content loads) but not filled in, so entries stay small and an author is loaded only where it is shown. Pass the pointer to getEntry to get the entry. It is not plain text either: it records the collection too, and Astro has checked it points somewhere.
11. Images, fonts and other files · The Image component does the work for you · QuizWhy does Astro write width and height on the <img> even though CSS may resize it?B. So the browser can reserve the right space before the file arrives, and nothing jumpsWith both numbers the browser knows the picture’s proportions and keeps a box of the right shape while it downloads. Without them the box is empty, then pops open, pushing the text down: a layout shift. They don’t change the file’s size; the resizing does that.
11. Images, fonts and other files · Picture offers several formats; the browser picks one · Find the bugThis card stops the build with an image error. Which line is wrong?Line 5: <Image src={pancakes} alt="Pancakes"A string src must be an address on the web: a full URL, or a path in public/ starting with /. ../assets/pancakes.jpg is a path in your project, so Astro stops with “Local images must be imported”. Pass the imported object: src={pancakes}. The import is also what lets Astro find, check and optimise the file.
11. Images, fonts and other files · Images from other sites must be allowed first · QuizWhy must you list the hosts whose images Astro may optimise?B. So nobody can use your site to resize and serve any image from anywhere, at your costOn a site rendered on demand, the image service takes an address and returns a resized file. Without a list, anyone could point it at any image on the internet and use your server as a free image converter. image.remotePatterns does the same with patterns (protocol, hostname with wildcards, path).
11. Images, fonts and other files · Images in content collections are checked too · Fill the blanksMake cover a checked image in the recipes collection.1: image, 2: image()The schema becomes a function that receives helpers; image() turns a relative path like cover: ./pancakes.jpg into image metadata. A misspelt file name fails the build, instead of shipping a broken picture. Keep the description (coverAlt) beside it so every cover has its alt.
11. Images, fonts and other files · Other files: ?url and ?raw · QuizSam wants /recipes.pdf to keep that exact address forever, because it is printed on flyers. Where does it go?B. public/recipes.pdfFiles in public/ are served at their own path, untouched. A ?url import gets a hashed name like /_astro/recipes.D8f2a.pdf that changes when the file does: perfect for caching, wrong for a printed address. src/pages/ is for routes, not documents.
12. Islands and client directives · By default, Astro sends no JavaScript · QuizWhich parts of a recipe page need to run in the browser?B. A timer that counts down while you cook; D. A box that scales the ingredients as you change the number of servingsThe timer and the servings box change what is on screen in response to the visitor, after the page has arrived: that needs code in the browser. A list and a link are just HTML; the browser handles a link click by itself.
12. Islands and client directives · A client directive turns a component into an island · QuizSam renders <Search recipes={list} /> with no directive. What does a visitor get?B. The search box and the full list as HTML, but typing filters nothingWithout a directive, Astro still renders the React component to HTML on the server and sends no JavaScript for it. That is useful for components that only display things. A search box needs to react to typing, so it needs a directive.
12. Islands and client directives · Check: choose the directive · QuizA likes counter sits at the very bottom of long recipe pages. Most visitors never scroll that far. Which directive?C. client:visibleWith client:visible its JavaScript is fetched only when it comes into view, so most visitors never download it. client:idle would still download it for everyone, just a little later. client:only would also skip the server HTML, leaving an empty gap until it loads.
12. Islands and client directives · Check: choose the directive · QuizWhat do you lose with client:only="react"?B. The server-rendered HTML: the island is empty until its JavaScript runsThe other directives send HTML first, so the island shows at once. client:only skips the server render, so there is a blank space (or a fallback you provide with slot="fallback") until the code loads. Use it only when the component truly can’t run on a server, for example a map library that reads window as soon as it is imported. The framework name is needed because Astro can’t guess it without rendering.
12. Islands and client directives · Props cross from server to browser as data · Find the bugThis island breaks. Which line causes it?Line 5: <Save client:visible slug={slug} />The Astro page only exists on the server, so a function created there can’t be sent to the browser: it is left out of the serialised props, and the island receives onSaved as undefined. Move the behaviour into the island itself: Save can log or show a message when it finishes. Pass islands data, not callbacks.
13. Rendering modes, adapters and deploying · The choice is made per route · QuizWhich pages of Sam’s blog need on-demand rendering?C. A page listing the recipes the current visitor saved, from their cookieOnly the saved-recipes page depends on who is asking: its cookie arrives with each request. The others are the same for everyone and only change when Sam edits content, so they are prerendered, and the next build updates them. Tags pages come from getStaticPaths, which also runs at build time.
13. Rendering modes, adapters and deploying · Check: personal data on a static page · QuizThe prerendered home page shows Hello, {name}, with name read from Astro.cookies. What do visitors see once it is deployed?B. Everyone sees the same HTML, made at build time when there was no cookieThe page was rendered once, during the build, with no request and so no cookies, and that one file is served to everybody. It never becomes personal. Make the route on demand with prerender = false, or read the value in the browser from an island.
13. Rendering modes, adapters and deploying · Check: what a hybrid build produces · Put in orderA visitor opens /saved on a static-default site with the Node adapter. Put what happens in order.1. The browser sends a request for /saved, with its cookies 2. The server finds no prerendered file for /saved 3. It runs the saved.astro route’s code with the real request 4. The route reads the cookie and renders HTML 5. The server sends that HTML backPrerendered pages are answered straight from files and never reach your code. /saved exported prerender = false, so the build left it as code in the server, which runs it for every request. That is why reading cookies works there and not on static pages.
13. Rendering modes, adapters and deploying · Deploying: a folder of files, or a server · QuizA prerendered page shows the time it was rendered. Under astro dev the time changes on every refresh. What do visitors see once it is deployed?B. The time of the build, the same for everyone until the next buildIn development each page is rendered again whenever you open it, so its code runs on every refresh. The build runs it once and saves the HTML. That difference hides bugs, which is why astro preview (or a staging deploy) belongs in your routine. Cookies are not one of those bugs: the dev server already leaves them out of prerendered pages, and warns you.
14. Endpoints, middleware and environment variables · Prerendered endpoints become files · QuizOn a fully static site with no adapter, Sam adds export const POST to likes.ts. What happens when a visitor sends a POST?B. Nothing runs: the host only serves the files the build made, and POST was never part of themA static build calls only GET, once, to make a file. There is no server afterwards to run code when a request arrives, so a POST has nothing to reach. Handling POST needs the route on demand (prerender = false) and an adapter.
14. Endpoints, middleware and environment variables · locals carry information to the route · Fill the blanksIn an on-demand recipe page, read the visitor’s units.1: Astro.localsPages reach locals through the Astro global; endpoints and middleware get it on the context argument. It belongs to this request only, so one visitor’s units never leak to another.
14. Endpoints, middleware and environment variables · On static pages, middleware runs at build time · QuizMiddleware sends visitors without a member cookie away from /members/. The members pages are prerendered. What happens in production?B. The check ran once at build time; the files are served to anyone who asksAt build time there was no cookie, so the middleware either redirected the build itself or let it through, and afterwards nothing runs when the file is fetched. A check on each visit needs those routes on demand (prerender = false), so the middleware runs for every request.
14. Endpoints, middleware and environment variables · Check: where a secret may go · Find the bugThe comments island must never expose the token. Which line is the problem?Line 2: // No import: the endpoint adds the token on the serverIsland code is sent to the browser, where anyone can read it. Astro refuses astro:env/server there. The design is already right otherwise: the island calls your own endpoint, /api/comments, which runs on the server, adds the token, and calls the comments service.
15. Forms and actions · Check: what a form sends · Find the bugThe server always receives an empty email. Which line is wrong?Line 3: <input id="email" name="email" type="email" required />Only inputs with a name are included in the form data. id links the label to the input; it isn’t sent. Add name="email" and the server gets the address under the key email.
15. Forms and actions · Check: what a form sends · QuizThe input has type="email" and required. Does the server still need to check the email?B. Yes: anyone can send a request without your form, with any dataBrowser checks are a kindness to visitors: quick feedback. They are not protection, because a request can be made by any program, skipping the form entirely. The server must validate everything it receives.
15. Forms and actions · Point a form at the action · Fill the blanksRead the result of the subscribe action in the page.1: getActionResult, 2: subscribegetActionResult takes the action itself (not a string), so the result is typed: result.data.email is known to be a string. It is undefined when the page was opened normally, without a form post.
15. Forms and actions · Throw ActionError for problems the visitor should hear about · QuizWhich result does a visitor get when they submit email=hello (no @)?B. result.error is an input error, and fields.email says why; the handler never runsThe schema is checked before the handler. z.email() fails, so Astro returns an input error with a message per field, and your code isn’t called with bad data. That is the main gift of actions: the handler can assume its input is valid.
15. Forms and actions · Progressive enhancement: works first, then better · Put in orderPut a no-JavaScript action form submission in order.1. The visitor presses Subscribe 2. The browser posts the form data to the page 3. Astro checks the data against the action’s schema 4. The handler runs with valid input 5. The page renders, reading getActionResult 6. The browser shows the new page with the messageThe browser does the sending; Astro validates before your handler; the page renders once the action has finished, so getActionResult already has the result. If validation fails, step 4 is skipped and the page renders with the input error instead.
16. Languages and view transitions · Tell Astro which locales exist · QuizWith the config above, where does the English about page live?B. /about/With prefixDefaultLocale: false, the default language keeps the plain addresses and only the others get a prefix. /en/about/ would only exist with prefixDefaultLocale: true. Astro never makes a page appear in two places on its own: every address still comes from a file.
16. Languages and view transitions · You still write the pages for each language · Fill the blanksComplete the Spanish about page so it reuses the shared component.1: es, 2: esThe folder name must match the locale code in locales exactly, so the page is src/pages/es/about.astro and its address is /es/about/. The component gets the locale as a prop and picks its words with it.
16. Languages and view transitions · Build every link from the config · Find the bugThe config has prefixDefaultLocale: false. One link is broken. Which line?Line 5: <a href={getRelativeLocaleUrl('en', 'recipes/')}>Recipes</a>English lives at the plain addresses here, so /en/recipes/ is a 404. Typing addresses by hand bakes in today’s routing rules; the helper asks the config every time.
16. Languages and view transitions · Guessing the visitor’s language needs a request · QuizA prerendered home page redirects to /es/ when Astro.preferredLocale === "es". What happens for a visitor whose browser prefers Spanish?B. Nothing: the page was built with no request, so preferredLocale was undefined and no redirect was writtenPrerendered code runs once, during the build, for nobody in particular. To act on Accept-Language, render that one route on demand, or let the visitor choose with a language picker (which is also kinder: people often read a language their browser doesn’t list first).
16. Languages and view transitions · The ClientRouter swaps pages without a full load · Put in orderPut a ClientRouter navigation in order.1. The visitor clicks a link to another recipe 2. The router fetches the new page’s HTML 3. The old and new pages are swapped, animated by named elements 4. astro:after-swap fires, then the new page’s astro:page-loadThe router intercepts the click, so the browser never does a full load. It fetches, swaps (running the animation), then fires its events: astro:after-swap right after the swap, astro:page-load once the page is ready. Scripts that only run at load would miss every page after the first.
16. Languages and view transitions · Choosing between CSS and the router · QuizSam wants recipe photos to morph from the list into the recipe page, and the site has a strict CSP. What fits best?B. @view-transition { navigation: auto; } plus a matching view-transition-name on both pagesThe morph needs only paired names, which native transitions support. The router would also do it, but it isn’t supported with Astro’s CSP and adds JavaScript for nothing here. Reach for the router when something must survive navigation, like audio that keeps playing.
17. Performance and caching · The fastest JavaScript is the JavaScript you don’t send · QuizEvery island on the recipe page uses client:load. The page feels slow to respond on a cheap phone. What is the best first change?B. Give each island the latest directive that still works, and drop the islands that only show static contentclient:load hydrates everything at once, as the page loads, so the phone is busy exactly when the visitor first taps. Deferring with idle and visible, and removing islands that don’t need to move, cuts that work. Changing framework is a big job for a smaller win; on-demand rendering changes where HTML is made, not how much JavaScript runs.
17. Performance and caching · Images and fonts decide LCP and CLS · Find the bugVisitors see the recipe text jump down as the page loads. Which line causes it?Line 6: <img src="/photos/lemon-cake.jpg" alt="A lemon cake" width="1200" height="800" />A plain <img> from public/ has no width or height, so the browser gives it no space until the file loads, then pushes everything below it down: layout shift (CLS). <Image> with an imported file knows the size and writes both attributes. If you must use <img>, write the dimensions yourself.
17. Performance and caching · How CSS and HTML reach the browser · QuizA blog with 200 posts shares one 60 kB stylesheet. Someone sets inlineStylesheets: "always". What happens as a reader moves from post to post?B. Each page’s HTML carries the 60 kB again, instead of reusing one cached fileInlined CSS is part of each HTML page, and HTML is not cached for long, so every page repeats it. A separate file with a hashed name is downloaded once and reused. Inlining wins for small CSS on the first visit; auto makes that call per stylesheet.
17. Performance and caching · Prefetch the page they are about to open · Fill the blanksPrefetch the next recipe as soon as its link is visible.1: prefetch, 2: viewportviewport suits the one obvious next step. Prefetching spends the visitor’s data, so don’t put load or viewport on every link in a long list: hover waits for a sign of intent.
17. Performance and caching · Astro 7 caches on-demand pages for you · QuizWith maxAge: 300, swr: 60, a visitor asks for a recipe 320 seconds after it was cached. What do they get?B. The cached copy straight away, while a fresh one is rendered in the backgroundAfter 300 s the copy is stale, but for 60 s more it may still be served while the server renders a new one behind the scenes. That visitor doesn’t wait; the next one gets the fresh page. Past 360 s, a request waits for a new render.
17. Performance and caching · Purge by tag when content changes, and never cache what’s personal · Find the bugThis page leaks one reader’s saved recipes to others. Which line?Line 5: Astro.cache.set(false)The cache keys on the address, and every reader asks for the same one. The first reader’s list is cached for an hour and shown to everyone after them. Personal responses opt out with set(false). Shortening maxAge only shrinks the leak.
18. Security and the Content Security Policy · Expressions are escaped; set:html is not · Find the bugReaders can post comments. Which line lets one reader run a script on everyone else’s screen?Line 8: <p>{c.body}</p>The name is escaped, so it’s fine; the body is written as raw HTML, so <img src=x onerror=…> in a comment runs in every reader’s browser. Render it as an expression, or sanitise it before using set:html. A Content Security Policy (later) is the safety net, not the fix.
18. Security and the Content Security Policy · Secrets stay on the server · QuizWhich of these puts the mail key where readers can see it?B. Passing MAIL_API_KEY as a prop to a client:visible islandAn island’s props are serialised into the HTML so the browser can hydrate it (module 12): the key would be in the page source. Actions, endpoints and middleware run on the server and only send back what you return. Let the island call an action that uses the key, rather than holding the key.
18. Security and the Content Security Policy · Astro checks where form posts come from · QuizDoes checkOrigin protect a prerendered page?B. No: a prerendered page is a file the host serves, and no Astro code runs for that requestThe check runs in Astro’s server, so it only covers routes rendered on demand. A static page can’t receive a form post anyway: forms should post to an action or endpoint, which is on demand and therefore checked.
18. Security and the Content Security Policy · Astro hashes every script and style it builds · QuizWhy does a hash-based policy suit a prerendered blog better than nonces?B. A nonce must change for every response, which needs a server per request; a hash depends only on the content, so it can be written into the file onceA nonce only protects you if it is unpredictable, so the HTML must be made fresh each time. A script’s hash is the same for every visitor, so a static file can carry its own policy and still be cached by a CDN.
18. Security and the Content Security Policy · You write the other directives · QuizA new island shows the weather from https://weather.example.org. It works in astro dev. What happens in the built site?B. The browser blocks the fetch under connect-src, and the island shows its error stateThe dev server has no CSP, so the problem hides there. In the build, connect-src lists only the site and the search service. Add the host deliberately, or fetch the weather on the server (an endpoint) so the browser only talks to your own site.
18. Security and the Content Security Policy · Scripts Astro can’t see need your hash · Fill the blanksHash the inline script and add it to this page’s policy.1: sha256, 2: base64, 3: insertScriptHashA CSP hash is the base64 of the digest, prefixed with the algorithm (sha256-). A hex digest looks similar but never matches, so the script is refused.
18. Security and the Content Security Policy · What a meta policy can’t do, and what it rules out · Find the bugThis config is meant to stop other sites framing the blog. Which line does nothing?Line 5Astro’s policy is a <meta> tag, and browsers ignore frame-ancestors there, so the page can still be framed. Send it as a header instead.
19. Integrations, the build pipeline and architecture · What astro build does, in order · Put in orderPut these build steps in order.1. Integrations’ astro:config:setup hooks run 2. Content collections are loaded and types generated 3. Client JavaScript and CSS are bundled with hashed names 4. Static pages are rendered to HTML 5. astro:build:done runs with the list of pagesConfig comes first because it decides everything else (integrations can still change it in setup). Content must be loaded before pages can query it. Pages are rendered once bundles exist, so the HTML can point at their hashed names. astro:build:done is last: the files are on disk.
19. Integrations, the build pipeline and architecture · Astro is built on Vite · QuizA pull request passes astro build but a prop is passed with the wrong type in an .astro file. Which command in CI would have caught it?B. astro checkastro build doesn’t type-check: it transforms code and renders pages, and a wrong type often still renders. astro check runs the TypeScript checker over .astro files too. sync only generates types; it doesn’t report misuse.
19. Integrations, the build pipeline and architecture · Writing a small integration · Fill the blanksUse the integration in the config.1: integrations, 2: pageList()Integrations go in the integrations array, called so they return their object. Order matters when two integrations touch the same thing: they run in the order listed.
19. Integrations, the build pipeline and architecture · Testing components with the container API · QuizWhat is the container API good for?B. Rendering a component with chosen props and slots in a test, and checking the HTMLIt is about isolation: one component, chosen inputs, the HTML it makes. Nothing to do with Docker containers despite the name. For the whole page in a real browser, use an end-to-end tool such as Playwright against astro preview.
19. Integrations, the build pipeline and architecture · Architecture: decide where each part runs · QuizSam adds comments to recipes. New comments should appear within a minute. Which design fits best?B. Keep the recipe static and load comments in a server island or a small endpoint, cached for about a minuteThe recipe itself rarely changes, so it stays a fast static file. Only the comments need freshness, and a short shared cache is fine because they are the same for every reader. A full rebuild per comment is slow; uncached on-demand pages pay for the server on every view; build-time props are stale until the next build.
20. Mastery challenge · How this challenge works · QuizIn a prerendered recipe page, which of these run in the reader’s browser?B. B: the search island’s component code; D. D: the <script>Frontmatter runs at build time only. A framework component without a directive is rendered to HTML on the server and sends no JavaScript, so C never runs in the browser. B hydrates there once the browser is idle, and a <script> tag is bundled and run by the browser.
20. Mastery challenge · Review: a dynamic route · Find the bugThe build stops on the first recipe page. Which line is wrong?Line 12: const { recipe } = Astro.propsWhatever getStaticPaths puts in props arrives in Astro.props under the same name. It sends recipe, the page reads post, which is undefined, so render(post) throws. With an interface Props declared (module 5), TypeScript and astro check would have flagged it before the build.
20. Mastery challenge · Review: a dynamic route · QuizSam adds src/content/recipes/plum-tart.md to the deployed, fully static site. When does /recipes/plum-tart/ exist?B. After the next build and deployA static site is the files made by the last build. getStaticPaths ran then, and plum tart wasn’t there. The dev server watches files; production doesn’t. Making the route on demand would also work, but for content that changes when Sam publishes, a rebuild is the simple answer.
20. Mastery challenge · Predict: an island’s props · QuizWhat happens with this island?B. Astro can’t serialise the function prop, so this doesn’t work as writtenIsland props are written into the HTML as data and read back in the browser. Functions can’t be written that way, whatever the directive or syntax. Build the URL inside the island, or pass ready-made URLs. Also look at recipes: the whole entries, bodies included, end up in the page source. Pass only title and id.
20. Mastery challenge · Predict: an island’s props · Fill the blanksPass the island only what it needs.1: data, 2: id, 3: idleSchema fields live under data; the entry’s id comes from the file name. Small props mean a smaller page and nothing private by accident. idle suits a search box in the header; visible would do too if it sat lower down.
20. Mastery challenge · Choose: which routes need a server · QuizThe site has the default output: "static" and an adapter. Which routes should export prerender = false?B. A page greeting a signed-in reader by name; D. An endpoint that receives the CMS’s webhookA greeting depends on the visitor’s cookie, and a webhook is a POST that must run when it arrives: both need a request. The about page, tag pages and the RSS feed are the same for everyone and change only when content does, so they stay files. The adapter only makes on-demand rendering possible; prerender decides.
20. Mastery challenge · Choose: which routes need a server · Put in orderA reader signs up for the newsletter on an on-demand page. Put the steps in order.1. The browser posts the form to the page 2. Astro checks the request’s origin (checkOrigin) 3. Middleware runs and fills locals 4. The action validates the input and runs its handler 5. The page renders with Astro.getActionResult()The origin check comes before any of your code, so a cross-site post is rejected with 403 early. Middleware wraps everything that follows. A form action runs before the page renders, so the page can show the result or the errors.
20. Mastery challenge · Complete: middleware for the members area · Fill the blanksSend signed-out readers away from /members/, and let everyone else through.1: defineMiddleware, 2: locals, 3: redirect, 4: nextdefineMiddleware types the function. locals carries the user to pages for this one request. Returning a response (the redirect) skips the page; next() renders it. Remember that the members pages must be on demand: for prerendered pages, middleware runs once at build time, with no reader.
20. Mastery challenge · Complete: middleware for the members area · QuizThe middleware above is in place, but /members/ was left prerendered. What does a signed-out reader get in production?B. The members page: it was built once, with no user, and is served as a file without running middlewareAt build time there is no cookie, so user was empty and the redirect happened while building, or the page was built as if public, depending on the code. Either way, later requests for the file never reach the middleware. Protection needs a request to protect: make the route on demand.
20. Mastery challenge · Find the bug: the newsletter action · Find the bugThe form posts with method="POST" action={actions.subscribe}, and every submission comes back with an error. Which line?Line 6: subscribe: defineAction({ accept: 'form',An action accepts JSON by default. An HTML form sends form data, so Astro rejects every submission (UNSUPPORTED_MEDIA_TYPE: the action only accepts JSON) before the schema is even checked. accept: "form" tells the action to parse FormData and check it against input. The secret is fine: it comes from astro:env/server and only runs in the handler.
20. Mastery challenge · Review a pull request · QuizWhich review comments are right?A. set:html on reader content is an XSS hole: render it as {tip.body} or sanitise it; B. client:visible would delay the island’s JavaScript until readers reach the tips; C. Caching a page that shows a reader’s own saved tips would show them to other readers; D. frame-ancestors is ignored in Astro’s <meta> policy: send it as a headerThe first four are each real. The personal part should move to a server island or an island that fetches it, so the shared page can stay cached. The last is wrong: on-demand routes can query collections like any other route.
20. Mastery challenge · Design: a recipe site that grows · QuizWhich plan fits all four wishes?B. Static recipes in en and es folders with i18n routing, a server island (or cached on-demand endpoint) for the seasonal box, native @view-transition CSS, and security.cspRecipes are the same for everyone, so they stay static in both languages. Only the seasonal box needs freshness, so only it touches a server, with a cache. The router isn’t supported with Astro’s CSP and 'unsafe-inline' would gut the policy, so animations come from native CSS. preferredLocale guesses a language; it doesn’t translate anything.
21. Final project: a conference website · Step 2: mark the current page · QuizSo that a page inside a section marks it too, you compare with here.startsWith(trim(href)) instead of ===. What does the nav show on /about/?B. Home and About, underlinedHome’s href is /, and every address starts with /, so Home now matches every page. About matches too, and Schedule doesn’t. startsWith suits a section like module 9’s recipes, but Home needs an exact match.
21. Final project: a conference website · Step 4: the schema checks every file · QuizWithout .regex(…), the file with start: 9:30 would load. Where would the opening keynote land on a schedule sorted by start?B. Last, after the closing keynote at 16:00The times are text, and text is compared a character at a time: "9" comes after "1", so "9:30" sorts after "16:00" (and after "13:30"). With two digits for the hour, text order and time order agree, and that’s what the pattern makes sure of. YAML does read 9:30: the error says “Invalid string”, so it was text that failed the pattern.
21. Final project: a conference website · Step 5: only the listed pages exist · Find the bugThis comes from a tutorial for an older Astro. In Astro 7, every talk page fails with Missing parameter: id. Which line is wrong?Line 7: params: { id: talk.id },Entries of a collection made by a loader have an id, and no slug: that was the old collections’ field. talk.slug is undefined, so no page gets an id, and the error names the parameter, not the line. npm run check points at it: slug doesn’t exist on the entry’s type.
21. Final project: a conference website · Step 7: link each talk to its tags · QuizEight talks have 13 tags between them, and three of them have css. How many pages does src/pages/tags/[tag].astro make?C. 6, one per different tagThe Set keeps each tag once, so css makes one page however many talks have it: the build lists six /tags/… pages. The pages are made at build time, one per entry getStaticPaths returns; nothing is worked out when someone visits, which is why /tags/php/ is the 404 page.
21. Final project: a conference website · Step 9: check the types · QuizIn TalkCard.astro you type {data.minute} min instead of {data.minutes} min. What happens?C. npm run build succeeds, every card loses its length, and npm run check reports itNeither the dev server nor the build checks types: data.minute is undefined, which draws as nothing, so the cards say “· Main hall · min”. astro check stops at it: error ts(2551): Property 'minute' does not exist on type '{ title: string; … minutes: number; … }'. Did you mean 'minutes'? The schema checks the files, not your code. Run the check before every build.