Agent context packet

Structured metadata, source alternates, graph links, headings, series position, and diagram inventory for crawlers and agent readers.

Table of contents

  1. Anatomy of an .astro File
  2. The Code Fence
  3. The Template
  4. Template Rules
  5. Scoped Styles
  6. Real Example: krowdev’s PostCard
  7. Putting It Together
  8. Sources

Series context

Learn Astro 6

A ground-up guide to Astro for developers coming from Python, LaTeX, or scientific computing.

  1. The Mental Model
  2. File-Based Routing
  3. .astro Files
  4. Components
  5. Astro Layouts
  6. Content Collections
  7. Astro Styling
  8. Markdown and Code Blocks
  9. Build and Deploy

Entry facts

Kind
guide
Maturity
evergreen
Confidence
high
Origin
ai-drafted (AI-drafted, human-reviewed)
Author
Agent
Directed by
krow
Published
Modified
Words
1,046 (5 min read)
Series
learn-astro #3
Tags
astro, fundamentals
Prerequisites
Full corpus
/llms-full.txt
Readable corpus
/source/full-corpus/

Graph links

Prerequisites astro-file-routing

Tagsastro, fundamentals

.astro Files

The core building block — a code fence that runs at build time and a template that outputs HTML.

/ directed by / / 5 min read
On this page

Anatomy of an .astro File

This builds on the Astro mental model and file routing. The next concepts are components, layouts, and styling.

Every .astro file has two parts, separated by a code fence (---):

---
// PART 1: Component Script
// Runs at BUILD TIME on your machine
// This is TypeScript/JavaScript
const greeting = "Hello";
const items = ["one", "two", "three"];
---
<!-- PART 2: Component Template -->
<!-- Outputs HTML -->
<!-- Curly braces {expression} insert values -->
<h1>{greeting}, world</h1>
<ul>
{items.map(item => <li>{item}</li>)}
</ul>
Analogy

Think of it as a Jupyter notebook with exactly two cells:

  1. Code cell (the fence) — compute values, import data, do logic
  2. Markdown/HTML cell (the template) — display the results

The code cell runs once. The output cell is the HTML that ships to visitors.

The Code Fence

Everything between the --- markers runs at build time only. It never reaches the browser. You can:

---
// Import other components
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
// Import data from content collections
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
// Use any JavaScript — fetch APIs, read files, compute
const sorted = posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
const total = sorted.length;
// Define TypeScript interfaces
interface Props {
title: string;
}
// Access page props
const { title } = Astro.props;
---
Key Insight

You can await at the top level in the code fence — no async wrapper needed. This is because the code runs at build time in Node.js, not in the browser.

The Template

Below the fence is HTML with {expressions} for dynamic values:

---
const name = "krow";
const isPublished = true;
const tags = ["astro", "webdev"];
---
<!-- Insert a value -->
<h1>Welcome to {name}dev</h1>
<!-- Conditional rendering -->
{isPublished && <span class="badge">Published</span>}
<!-- Ternary -->
<p>{isPublished ? "Live" : "Draft"}</p>
<!-- Loop (map returns elements) -->
<ul>
{tags.map(tag => <li>{tag}</li>)}
</ul>
<!-- Use a component -->
<Header />

Template Rules

SyntaxWhat It DoesPython Equivalent
{value}Insert a valuef"{value}"
{condition && <el/>}Render if truthyf"..." if condition else ""
{arr.map(x => <li>{x}</li>)}Loop[f"<li>{x}</li>" for x in arr]
<Component />Use another .astro fileCalling a function
<slot />Where child content goes*args placeholder
Gotcha

There’s no if/else block in the template — use ternaries (cond ? a : b) or && for conditionals. There’s no for loop — use .map(). This comes from JSX conventions.

Scoped Styles

A <style> tag in an .astro file is scoped by default — its CSS only affects that component, not the rest of the page:

---
---
<h1>Title</h1>
<p>Body text</p>
<style>
/* This ONLY applies to the h1 in THIS file */
h1 {
color: purple;
font-size: 2rem;
}
</style>

Astro achieves this by adding unique data-astro-* attributes to elements and selectors at build time. You never see this — it just works.

Analogy

Scoped styles are like namespacing in Python. h1 in one file doesn’t collide with h1 in another, just as module_a.process() doesn’t collide with module_b.process().

For styles that apply everywhere, use a separate .css file imported in a layout (like krowdev’s global.css).

Real Example: krowdev’s PostCard

Here’s an actual component from this site — PostCard.astro. Toggle between the source code you write, the HTML Astro compiles it to, and what the browser displays:

src/components/PostCard.astro

---
interface Props {
title: string;
description?: string;
date: Date;
tags?: string[];
href: string;
}
const { title, description, date, tags = [], href } = Astro.props;
const formatted = date.toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric',
});
---
<a href={href} class="post-card">
<article>
<time datetime={date.toISOString()}>{formatted}</time>
<h3>{title}</h3>
{description && <p>{description}</p>}
{tags.length > 0 && (
<div class="tags">
{tags.map(tag => <span class="tag">{tag}</span>)}
</div>
)}
</article>
</a>
<!-- What Astro outputs after build (simplified) -->
<a href="/article/welcome-to-krowdev/" class="post-card" data-astro-cid-kg7m>
<article data-astro-cid-kg7m>
<time datetime="2026-03-15T00:00:00.000Z" data-astro-cid-kg7m>
Mar 15, 2026
</time>
<h3 data-astro-cid-kg7m>Welcome to krowdev</h3>
<p data-astro-cid-kg7m>A second brain for everything learned...</p>
<div class="tags" data-astro-cid-kg7m>
<span class="tag" data-astro-cid-kg7m>meta</span>
</div>
</article>
</a>

Notice what changed:

  • {formatted} became Mar 15, 2026 — computed at build time
  • {date.toISOString()} became the full ISO string — computed at build time
  • {tags.map(...)} expanded to one <span> per tag
  • data-astro-cid-* attributes were added for scoped CSS
  • No JavaScript shipped — it’s all static HTML

Putting It Together

Here’s how krowdev’s homepage actually works:

src/pages/index.astro
---
// 1. Import layout and content API
import Base from '../layouts/Base.astro';
import { getCollection } from 'astro:content';
// 2. Fetch and sort data at build time
const recent = (await getCollection('kb'))
.sort((a, b) => b.data.created.valueOf() - a.data.created.valueOf())
.slice(0, 3);
---
<!-- 3. Wrap in Base layout (provides <html>, header, footer) -->
<Base title="krowdev">
<section class="hero">
<h1>krowdev</h1>
</section>
<!-- 4. Render dynamic data -->
{recent.map(entry => (
<a href={`/${entry.data.kind}/${entry.id}/`}>
<h3>{entry.data.title}</h3>
</a>
))}
</Base>
<!-- 5. Scoped styles for this page only -->
<style>
.hero { padding: 5rem 1.5rem; text-align: center; }
</style>
Challenge: Read a real .astro file

Open krowdev’s PostCard.astro:

Terminal window
cat src/components/PostCard.astro

Identify:

  1. What props does it accept? (Look at the interface Props)
  2. What computation happens in the code fence?
  3. What template logic renders the tags? (Hint: look for .map())
Answer
  1. Props: title (string), description (optional string), date (Date), tags (optional string[]), href (string)
  2. Computation: Formats the date using toLocaleDateString()
  3. Tags: {tags.map(tag => <span class="tag">{tag}</span>)} — maps each tag string to a styled <span>

Sources

Diagram

Drag to pan · scroll or pinch to zoom · Esc to close