Views & Rendering Trusted Content

Website Rebuild with Astro

Chapter 4 · Views & Rendering Trusted Content

This chapter builds the real layout Chapter 3's own database-backed route uses — and, like every sibling course before it, needs a way to render stored HTML content as real markup, not escaped text.

The Layout

// src/layouts/PageLayout.astro --- import { db } from '../db/client'; import { pages } from '../db/schema'; import { eq } from 'drizzle-orm'; interface Props { page: typeof pages.$inferSelect; } const { page } = Astro.props; async function getBreadcrumb(pageId: number) { const crumbs = []; let currentId: number | null = pageId; while (currentId) { const [current] = await db.select().from(pages).where(eq(pages.id, currentId)); if (!current) break; crumbs.unshift(current); currentId = current.parentId; } return crumbs; } const breadcrumb = await getBreadcrumb(page.id); --- <html lang="en"> <head><title>{page.title}</title></head> <body> <nav> {breadcrumb.map((crumb) => <a href={`/${crumb.fullPath}`}>{crumb.title}</a>)} </nav> <h1>{page.title}</h1> <div set:html={page.body} /> </body> </html>
The same N+1 pattern, planted deliberately a sixth time
getBreadcrumb walks current.parentId one row at a time inside a while loop — one database query per ancestor level, exactly the pattern every sibling rebuild course (and the standalone astro1 course itself) built into its own breadcrumb code on purpose. It's left as written here, to be caught and fixed in Chapter 6.

set:html: Astro's Own Answer

The same job, five different syntaxes
set:html={page.body} sets an element's HTML content directly, bypassing Astro's own default auto-escaping — the exact same job Django's |safe filter, Laravel's {!! !!}, and Rails' <%== %> each do in their own frameworks. Astro's version is a template directive on the element itself, rather than a filter or a special output-tag syntax — its own genuine syntactic shape, doing an identical job to every sibling.
Only for content the admin controls
set:html is only safe for content this project's own admin interface writes — never for rendering arbitrary user input directly, since it bypasses the exact escaping that protects against injected markup. The same warning every sibling course's own equivalent mechanism carries.

Compared Across the Series

FrameworkMechanism
Django{{ page.body|safe }} — a template filter
Laravel{!! $page->body !!} — a Blade output-tag variant
Rails<%== @page.body %> — an ERB output-tag variant
Astro<div set:html={page.body} /> — a template directive on the element

Hands-On Exercises

Exercise 1

Build PageLayout.astro rendering a page's title and body via set:html, and confirm real stored HTML (e.g. a <strong> tag inside body) renders as actual formatted HTML, not escaped text.

📄 View solution
Exercise 2

Build getBreadcrumb() and confirm it produces the correct, correctly-ordered ancestor chain for a real page stored at least three levels deep.

📄 View solution
Exercise 3

Temporarily replace set:html={page.body} with plain {page.body} interpolation, and confirm Astro's default behavior actually escapes the stored HTML — the raw tags appear as visible text instead of rendering.

📄 View solution

Chapter 4 Quick Reference

  • set:html={page.body} — Astro's own directive-based equivalent of |safe/{!! !!}/<%== %>
  • Only for admin-controlled content — never for arbitrary user input
  • getBreadcrumb — a deliberately planted N+1 pattern, caught in Chapter 6
  • Plain { } interpolation escapes by defaultset:html is a genuine, deliberate opt-out
  • Next chapter: Styling — Dark Theme