Workshop | Astro

Workshop | Astro

Von am 16.11.2025

Im 3. Semester habe ich einen Workshop zu Astro gehalten. Dabei habe ich erklärt, was das Webframework ausmacht, und anhand von einem praktischen Beispiel die wichtigsten Kernfunktionen demonstriert.

Was ist Astro?

Astro ist ein Framework für contentfokussierte Webprojekte, wie z.B. Blogs, E-Commerce- oder Marketing-Websites. Es setzt auf eine komponentenbasierte Web-Architektur, was bedeutet, dass das meiste als statische HTML-Seite gerendert wird und man kleine Islands mit JavaScript für Interaktivität hinzufügen kann. Astro verfolgt einen Server-first-Ansatz und kommt standardmäßig ohne JavaScript am Client aus, liefert aber bei Bedarf Unterstützung für Frameworks wie React, Vue, Svelte, Solid etc. Darüber hinaus bietet Astro Content Collections, mit denen sich Markdown-Inhalte strukturiert organisieren und mithilfe von TypeScript typsicher validieren lassen.

Projekt erstellen

npm create astro@latest (basic, helpful starter Projekt auswählen)

In VS Code gibt es eine Extension namens Astro, die hilfreich ist.

Ordnerstruktur (als Beispiel):

  • assets
  • components
  • content (für Content Collections)
  • layouts
  • pages (file-based Routing)
  • styles

Dateiaufbau und Astro-Komponenten

  • Dateiendung: .astro
  • Codeausführung:
    • serverseitig: --- --- (Frontmatter)
    • clientseitig: <script></script>
  • JSX-ähnliche Ausdrücke
    • {variable} in HTML
    • Aber: Funktionen und Objekte können so nicht übergeben werden (wie z.B. bei React), stattdessen mit Script-Tag umsetzbar

Beispielsweise MyAstroComp.astro:

---
// Script mit JS, das am Server ausgeführt wird
const text = "Hello World";
---
<!-- Hier Template der Komponente mit HTML, CSS und JS -->
<div>{text}</div>

Was nicht funktioniert:

---
function handleClick() {
  console.log("clicked");
}
---
<button onclick="handleClick()">{text}</button>

Komponente in index.astro verwenden:

---
import MyAstroComp from "../components/MyAstroComp.astro";
---
<MyAstroComp />
  • <slot /> für Children (wie {children} in React)
  • Mit Astro.props auf Properties zugreifen
---
interface Props {
  name: string;
}
const { name } = Astro.props;
---
<h2>Hello {name}!</h2>

Unterschiede Astro vs. JSX

React hinzufügen

Interaktivität in Astro Komponenten einzubauen ist mühsam. Um mit einem Buttonklick umgehen zu können, müsste man ein <script> Tag hinzufügen. Stattdessen kann man aber auch ein beliebiges anderes Framework zum Projekt hinzufügen und so auch beispielsweise React-Komponenten bauen und nutzen.

Um React als Beispiel hinzuzufügen: npx astro add react

Achtung: Komponente wird by default nur am Server gerendert!

Für Interaktivität braucht es Hydration und eine Client Directive.

Client Directives

Die client:* Directive gibt an, wann das JavaScript an den Browser geschickt werden soll. Die Komponente wird zuerst am Server gerendert (außer bei client:only), dann wird das JS gesendet und somit wird die Komponte hydrated und interaktiv.

  • client: load
    • JS: sofort beim Laden der Seite
  • client: idle
    • JS: nach initialem Ladeprozess und wenn requestIdleCallback-Event ausgelöst wird
  • client: visible / client:visible={{rootMargin}}
    • JS: sobald Komponente bzw. angegebener Margin sichtbar ist
  • client:media={string}
    • JS: wenn CSS media query erfüllt wird (z.B. nur für bestimmte Bildschirmgrößen)
  • client:only={string}
    • Überspringt SSR und rendert Komponente nur am Client. Framework muss als String übergeben werden.

Die Client Directive wird dort hinzufügt, wo die Komponente verwendet wird.

---
import MyReactButton from "../components/MyReactButton";
---
<MyReactButton client:load />

Output Type für Build

In der astro.config Datei kann der Output Type festgelegt werden. Dieser bestimmt, wann die Seiten standardmäßig gerendert werden (beim Builden oder on demand). Das gilt für alle Seiten, außer bei denen eine Ausnahme festgelegt ist.

In astro.config.mjs: output: 'static' oder 'server'

  • static (default): SSG, Seiten werden vorgerendert (prerendered); Ergebnis: statische Website
  • server: SSR, Seiten werden on demand gerendert; Ergebnis: server-rendered Website
  • Für einzelne Seite ändern: export const prerender = false
    • wenn in config static: false
    • wenn in config server: true

Wichtig: Wenn eine Seite SSR verwendet, muss ein Server-Adapter (z.B. node) festgelegt werden: npx astro add node

Blog als Praxisbeispiel

Als Beispiel wird ein kleiner, einfacher Blog programmiert. Dieser beinhaltet zwei Seiten, wobei auf einer alle Einträge mit Bild, Datum und Titel aufgelistet werden (Übersichtsseite) und auf der anderen die Informationen des jeweiligen Eintrags stehen (Detailseite). Die einzelnen Blogbeiträge sollen in Form von Markdown-Dateien erstellt, bearbeitet und verwaltet werden können. Auf das Styling wird bei diesem Beispiel nicht eingegangen.

Content Collection erstellen

Content Collections helfen dabei, Markdown-Dateien zu organisieren. Genau das wird für die Blogbeiträge benötigt. Um eine Collection zu erstellen, muss diese in src/content.config.ts definiert werden.

import { glob } from "astro/loaders";
import { defineCollection, z } from "astro:content";

const blog = defineCollection({
  loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
  schema: () =>
    z.object({
      title: z.string(),
      date: z.coerce.date(),
      img: z.string(),
    }),
});

export const collections = { blog };

Die Funktion defineCollection() verlangt dabei zwei Parameter, einerseits den Loader und andererseits optional das Schema. Der Loader legt fest, wie und von wo die Inhalte der Collection geladen werden. Astro stellt dabei zwei integrierte Varianten zur Verfügung:

  • glob(): erstellt Einträge aus Ordnern mit Dateien
  • file(): erstellt Einträge basierend auf einer lokalen Datei (z.B. JSON-File)

Das Schema wird mit zod erstellt. Wird ein Schema definiert, so werden die entsprechenden Markdown-Dateien dahingehend validiert. Außerdem können generierte Types verwendet werden (z.B. type Props = CollectionEntry<"blog">["data"];). Wichtig ist nur, dass der Dev-Server neu gestartet wird, wenn sich Änderungen am Schema ergeben, sodass Astro das mitbekommt.

Ein Blogeintrag soll in diesem Fall immer einen Titel, ein Datum und ein Bild beinhalten, sodass diese Informationen auf der Übersichtsseite angezeigt und auf der Detailseite immer gleich formatiert werden können.

Blogbeiträge hinzufügen

Es können jetzt diverse Blogbeiträge in Form von Markdown-Dateien unter content/blog erstellt werden (Pfad in content.config.ts festgelegt).

Beispiel: first-entry.md

---
title: "First Entry"
date: "2025-10-30"
img: "/favicon.svg"
---
Hello, this is my first entry.

## H2

This is a paragraph.

Im Frontmatter (innerhalb der drei Bindestriche) werden die drei Parameter angegeben, darunter folgt der Inhalt. Als Bild wird der Einfachheit halber das favicon im public-Ordner hergenommen. Der Blogbeitrag kann Überschriften, Absätze, Links, Codeteile, Tabellen und alles, was Markdown zu bieten hat, beinhalten. Wichtig ist nur, dass man das entsprechende Styling dafür definiert (h2, p, code, a etc.).

MDX

Es können nicht nur md-Dateien erstellt werden, sondern auch mdx-Dateien, in denen die eigenen Komponenten (egal ob Astro, React etc.) verwendet werden können. Es muss nur die entsprechende Astro-Integration installiert werden (npx astro add mdx) und mdx in der Definition der Content Collection berücksichtigt werden (wurde bereits getan).

Beispiel: second-entry.mdx

---
title: "Second Entry"
date: "2025-10-31"
img: "/favicon.svg"
---

This is my second entry.

import MyReactButton from "../../components/MyReactButton"

<MyReactButton client:load />

components/MyReactButton.tsx

export default function MyReactButton(){

    function handleClick(){
        console.log("Hello, you clicked me")
    }

    return (
        <button onClick={handleClick}>Click me</button>
    )
}

Seiten erstellen

Astro verwendet file-based Routing. Die Ordner- und Dateistruktur im pages-Ordner bestimmt somit automatisch, unter welchen URLs die Seiten aufgerufen werden können. Im pages-Ordner wird ein Unterordner blog und darin index.astro und [slug].astro angelegt. Der Blog ist somit unter /blog erreichbar. index.astro ist eine statische Route, während [slug].astro eine dynamische Route ist. Der Parameter slug kann dabei durch eine beliebige andere Bezeichnung ersetzt werden. Für mehr Tiefe kann auch ein Rest-Parameter verwendet werden: […path].astro.

Übersichtsseite

Für die index Seite wird zuerst ein Layout erstellt. Ein Layout ist dabei nichts anderes als eine normale Astro-Komponente.

layouts/Layout.astro

---
import "../styles/global.css";
---

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>My Blog</title>
</head>
<body>
<slot />
</body>
</html>

<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
}
</style>

Für den Header kann ebenso eine Astro-Komponente erstellt werden (components/Header.tsx).

<header>
  <nav>
    <a href="/blog">My Blog</a>
  </nav>
</header>

Nun kann index.astro befüllt werden:

---
import { getCollection } from "astro:content";
import Header from "../../components/Header.astro";
import Layout from "../../layouts/Layout.astro";

const posts = (await getCollection("blog")).sort((a, b) => {
  return b.data.date.valueOf() - a.data.date.valueOf();
});
---

<Layout>
  <Header />
  <ul>
    {
      posts.map((post) => (
        <li>
          <a href={"/blog/" + post.id}>
            <div>
              <img src={post.data.img} />
            </div>
            <div>{post.data.title}</div>
            <div>{post.data.date.toLocaleDateString()}</div>
          </a>
        </li>
      ))
    }
  </ul>
</Layout>

Mithilfe von getCollection() kann eine Collection, sprich ein Array von Einträgen, geholt werden. In diesem Beispiel werden die Einträge von der Blog-Collection zusätzlich nach dem Datum sortiert.

Detailseite

Für die Detailseite wird ebenso ein Layout erstellt, das den Titel, das Bild und das Datum übergeben bekommt und anzeigt.

layouts/BlogLayout.astro

---
import type { CollectionEntry } from "astro:content";
import Header from "../components/Header.astro";

type Props = CollectionEntry<"blog">["data"];

const { title, date, img} = Astro.props;
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="generator" content={Astro.generator} />
    <title>My Blog</title>
  </head>
  <body>
    <Header />
    <main>
      <article>
        <div>
          <img src={img} />
        </div>
        <div>{date.toLocaleDateString()}</div>
        <h1>{title}</h1>
        <div>
          <slot />
        </div>
      </article>
    </main>
  </body>
</html>

<style>
  html,
  body {
    margin: 0;
    width: 100%;
    height: 100%;
  }
</style>

Die Detailseite muss nun den Parameter slug auslesen und die entsprechenden Informationen dem BlogLayout übergeben. Es gibt zwei Varianten, das zu tun, je nachdem ob man SSR oder SSG verwenden möchte.

Variante 1: SSR (on demand Rendering)

---
import { render } from "astro:content";
import { getEntry } from "astro:content";
import BlogLayout from "../../layouts/BlogLayout.astro";

export const prerender = false;
const { slug } = Astro.params;

if (!slug) {
  return Astro.rewrite("/404");
}

const entry = await getEntry("blog", slug);

if (!entry) {
  return Astro.rewrite("/404");
}

const { Content } = await render(entry);
---

<BlogLayout {...entry.data}>
  <Content />
</BlogLayout>

Mithilfe von Astro.params kann der Parameter slug ausgelesen werden. Mit getEntry() wird dann der entsprechende Eintrag geholt. Wenn es keinen passenden Eintrag gibt, wird 404 angezeigt. Um den Inhalt der Zielseite anzuzeigen, ohne die URL zu verändern, wird Astro.rewrite() genutzt.

Variante 2: SSG (statische Seite)

---
import type { GetStaticPaths } from "astro";
import { render } from "astro:content";
import BlogLayout from "../../layouts/BlogLayout.astro";
import { getCollection } from "astro:content";

export const getStaticPaths = (async () => {
  const entries = await getCollection("blog");

  return entries.map((entry) => ({
    params: { slug: entry.id },
    props: entry,
  }));
}) satisfies GetStaticPaths;

const entry = Astro.props;

const { Content } = await render(entry);
---

<BlogLayout {...entry.data}>
  <Content />
</BlogLayout>

Soll eine dynamische Route statisch sein, so muss sie eine Funktion namens getStaticPaths() exportieren, die ein Array an Objekten mit dem property params zurückgibt. So werden alle möglichen Paths vordefiniert. In diesem Beispiel holt man sich also alle Einträge der Blog-Collection in der getStaticPaths()-Funktion und über die Properties (Astro.props) können dann die Informationen des einzelnen Eintrags ausgelesen werden.

Fazit

Mit Astro können schnelle Websites erstellt werden. Wenn man bereits Erfahrung mit HTML, JSX oder React hat, so fällt einem der Einstieg in das Framework besonders leicht, da man viele vertraute Konzepte wiederfindet. Nur bei komplexeren Websites stößt Astro irgendwann auf seine Grenzen.

Quelle

https://docs.astro.build/en

Beitrag kommentieren

(*) Pflichtfeld