.astro Files
The core building block — a code fence that runs at build time and a template that outputs HTML.
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>Think of it as a Jupyter notebook with exactly two cells:
- Code cell (the fence) — compute values, import data, do logic
- 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 componentsimport Header from '../components/Header.astro';import Footer from '../components/Footer.astro';
// Import data from content collectionsimport { getCollection } from 'astro:content';const posts = await getCollection('blog');
// Use any JavaScript — fetch APIs, read files, computeconst sorted = posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());const total = sorted.length;
// Define TypeScript interfacesinterface Props { title: string;}
// Access page propsconst { title } = Astro.props;---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
| Syntax | What It Does | Python Equivalent |
|---|---|---|
{value} | Insert a value | f"{value}" |
{condition && <el/>} | Render if truthy | f"..." if condition else "" |
{arr.map(x => <li>{x}</li>)} | Loop | [f"<li>{x}</li>" for x in arr] |
<Component /> | Use another .astro file | Calling a function |
<slot /> | Where child content goes | *args placeholder |
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.
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}becameMar 15, 2026— computed at build time{date.toISOString()}became the full ISO string — computed at build time{tags.map(...)}expanded to one<span>per tagdata-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:
---// 1. Import layout and content APIimport Base from '../layouts/Base.astro';import { getCollection } from 'astro:content';
// 2. Fetch and sort data at build timeconst 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:
cat src/components/PostCard.astroIdentify:
- What props does it accept? (Look at the
interface Props) - What computation happens in the code fence?
- What template logic renders the tags? (Hint: look for
.map())
Answer
- Props:
title(string),description(optional string),date(Date),tags(optional string[]),href(string) - Computation: Formats the date using
toLocaleDateString() - Tags:
{tags.map(tag => <span class="tag">{tag}</span>)}— maps each tag string to a styled<span>
Sources
- Astro Docs, Astro syntax
- Astro Docs, Components
- MDN, Template literals