Übersicht
Teil 3 meiner Artikelserie beschreibt die Konfiguration von Astro für mein Blog und die Migration der “alten” Hugo-Artikel.
Hier noch die Links zu den anderen Artikeln dieser Serie:
- Teil: Basics, GitHub Repository
- Teil: AzureStatic Web App und Azure DNS
- Teil: Astro
- Teil: Tailwind CSS
Astro vorbereiten und installieren
Ich verzichte an dieser Stelle darauf, die Installation von Astro im Detail zu beschreiben, da die offizielle Dokumentation (Install Astro) sehr gut und die Schritte einfach nachvollziehbar sind.
Die erste Astro Page
Mein erster Schritt nach der Installation, war eine neue, sehr einfache Seite (Page) /pages/imprint.astro
anzulegen: das Impressum. Das Impressum besteht nur aus statischem Text ohne zusätzliche Funktionen und war eine gute Gelegenheit, die Grundfunktionen der Astro-Layouts zu erkunden.
Eine .astro
-Datei beginnt immer mit einem eigenen “Component Script (JavaScript)“-Block, der in ---
eingeschlossen ist. Die Impressum-Page besteht initial nur aus einer import
-Direktive, die eine Komponente aus dem layouts
-Verzeichnis referenziert und anschließend mit <Layout>...</Layout>
verwendet wird.
Die Layout-Parameter werden einfach in den Attributen des <Layout>
-Tags übergeben, wie zum Beispiel title="Impressum"
---
import Layout from "../layouts/BaseLayout.astro";
---
<Layout title="Impressum" description="Das rechtliche 'Kleingedruckte': Impressum und Datenschutz">
... HTML-Inhalt ...
</Layout>
Das Ergebnis ist erst mal recht unspektakulär und rendert unter /imprint
einfach den HTML-Inhalt von imprint.astro
unter Verwendung des referenzierten Layouts. Sehen wir uns daher dieses Layout näher an.
Ein einfaches Layout
Die sogenannten “Layouts” sind Astro Komponenten und beinhalten wiederverwendbare Vorlagen für HTML-UI-Elemente, wie zum Beispiel Kopf- und Fußzeilen oder Navigationsleisten.
Mein ursprüngliches BaseLayout
importiert erst mal drei weitere Komponenten:
BaseHead
enthält Metadaten für den HTML-<head>
, wie zum Beispiel Links auf die Schriftarten, das Favicon und den Seitentitel, der als Attributtitle
übergeben wird.- Die
Header
-Komponente enthält die Navigationsleiste und die Social-Media-Links am Seitenanfang. - Der
Footer
besteht eigentlich nur aus einem Link auf das Impressum und nochmal den Social-Media-Links.
Den eigentlichen Inhalt, der von einer (Page), wie zum Beispiel dem oben definierten imprint.astro
übergeben wird, können wir mit dem <slot />
-Tag an einer beliebigen Stelle einfügen.
Hier das vollständige Beispiel:
---
import BaseHead from "../components/BaseHead.astro";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
const { title, description } = Astro.props;
---
<!doctype html>
<html lang="de">
<head>
<BaseHead title={title} description={description} />
</head>
<body>
<Header />
<section>
<slot />
</section>
<Footer />
</body>
</html>
Sehen wir uns im nächsten Schritt einen der wichtigsten Grundsteine von Astro an:
Components
Die Components sind flexible “Grundbausteine”, die beliebig verschachtelt von Seiten (Pages) und anderen Komponenten verwendet werden können. Komponenten können nur HTML-Code, aber keine client-seitige runtime enthalten!
Im vorangegangenen Beispiel habe ich die Footer
-Komponente verwendet. Sehen wir uns diese im Detail an:
---
import SocialLinks from "./SocialLinks.astro";
const today = new Date();
---
<footer class="...">
©{today.getFullYear()} Thomas Hartl
| erstellt mit ♥️ und <a href="https://astro.build/" target="_blank">Astro</a>
| <a href="/imprint">Impressum und Datenschutz</a>
<SocialLinks />
</footer>
Im Component Script (zwischen den ---
) wird
- eine weitere Komponente
SocialLinks
importiert, welche die Social-Media-Links rendert und - eine Konstante
today
definiert, die das aktuelle Datum enthält.
Anschließend wird im HTML-Code ein einfacher <footer>
definiert. JavaScript-Code kann innerhalb eines {}
-Blocks ausgeführt werden, um zum Beispiel Variablen auszugeben. {today.getFullYear()}
rendert in diesem Beispiel die aktuelle Jahreszahl.
Mit diesen Bausteinen lässt sich schon mal eine grundlegende, statische Website bauen. Aber eigentlich wollte ich ja mein Blog, bestehend aus Markdown-Inhalt in Astro verwenden. Dazu brauchen weitere Bausteine:
Markdown
Die Blog-Beiträge des bisherigen Hugo-Blogs sind bereits im Markdown-Format. Ich kann sie daher direkt ins Verzeichnis /content
kopieren und die bisherige Blog-Struktur mit Unterordnern für Jahr und Monat beibehalten. Die Verzeichnisse sehen jetzt ungefähr so aus:
content/
+ blog/
+ 2020/
| + 08/
| - post1.md
| - post2.md
+ 2021/
+ 03/
- post3.md
[...]
Die Markdown-Dokumente bestehen aus einem zwischen ---
eingeschlossenen (“Codefence”) Header und dem Markdown-Inhalt selbst. Im Header sind die Metadaten des Beitrags definiert, genau so wie im bisherigen Hugo-Blog:
title
als Beitragstitel.description
als Kurzbeschreibung, die zum Beispiel in der Beitragsübersicht angezeigt wird.pubDate
beinhaltet das (ursprüngliche) Veröffentlichungsdatum eines Beitrags.updatedDate
hingegen optional ein Aktualisierungsdatum.- Mit
draft
kann ein Beitrag als “Entwurf” markiert werden. Entwürfe sollen auf der Seite nicht gerendert werden - dazu aber später mehr. - Das
heroImage
enthält einen Verweis auf das Beitragsbild. tags
ist eine Liste mit Stichworten.
Im nächsten Schritt müssen wir Astro beibringen, die Markdown-Dokumente zu laden und die Metadaten richtig zuzuordnen.
Content Collections
Mit den Content Collections können wir strukturierte Inhalte - in unserem Beispiel Dateien bzw. Markdown-Dokumente - in Astro verwenden.
Eine Content Collection definieren
Bevor wir eine Content Collection verwenden können, müssen wir sie erst mal in src/content.config.ts
definieren. In meinem Fall möchte ich alle Markdown-Dokumente (_.md und _.mdx) in meinem Blog-Verzeichnis /src/content/blog
laden. Dazu verwende ich den vordefinierten glob()
loader, der ein Verzeichnis (base
) nach Dateien mit einem bestimmten Muster (pattern
) durchsucht und lädt.
Zusätzlich benötigt jede Content Collection ein schema
, welches mit Zod validiert wird. Das Schema meiner Blog-Collection ist dasselbe, das ich schon im Hugo-Blog verwendet habe, wie oben beschrieben, mit ein paar Eigenheiten:
updatedDate
ist alsoptional
definiert.draft
ist ein ebenfallsoptional
er Boolean-Wert.tags
ist eine Liste, die zwar leer [], aber nichtnull
sein darf.
So sieht die Definition der Blog Collection inklusive des Schemas aus:
import { defineCollection, z } from "astro:content";
import { glob, file } from "astro/loaders";
const blog = defineCollection({
loader: glob({ base: "./src/content/blog", pattern: "**/[^_]*.{md,mdx}" }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
draft: z.boolean().optional(),
heroImage: z.string().optional(),
tags: z.array(z.string()),
}),
});
Die Blog-Collection abfragen
Nun, da wir eine Content Collection für unsere Markdown-Blog-Beiträge definiert haben können wir sie mit folgenden Astro-Funktionen abfragen:
getCollection()
liefert alle Einträge einer Collection als Array zurück. Genau das Richtige für die Übersichtsseite der Blog-Beiträge.getEntry()
liefert einen bestimmten Eintrag der Collection.
Im einfachsten Fall laden wir die gesamte Collection:
---
import { getCollection } from "astro:content";
const posts = await getCollection("blog");
---
Ein wenig komplizierter wird es, wenn wir nur Beiträge laden möchte, die nicht als draft
markiert sind. Dazu verwenden wir einen filter-callback:
---
import { getCollection } from "astro:content";
const posts = await getCollection("blog", ({ data }) => { return data.draft !== true; })
---
Da getCollection()
ein JavaScript-Array zurückgibt können wir die Beiträge mit der Array.sort() Funktion, und (optional) einer Vergleichsfunktion, nach beliebigen Attributen sortieren. Hier zum Beispiel nach dem updatedDate
bzw. wenn dieses nicht vorhanden ist nach pubDate
:
---
import { getCollection } from "astro:content";
const posts = (
await getCollection("blog", ({ data }) => { return data.draft !== true; })
).sort(
(a, b) =>
(b.data.updatedDate?.valueOf() ?? b.data.pubDate.valueOf()) -
(a.data.updatedDate?.valueOf() ?? a.data.pubDate.valueOf())
);
---
Mit der “Blog” Content Collection und den beiden Funktionen getCollection()
und getEntry()
Blog-Artikel aus Markdown rendern
Blog-Übersichtsseite
Zuerst bauen wir eine Übersichtsseite für die Blog-Artikel unter /pages/blog/index.astro
. Darin laden wir wie oben beschrieben mit getCollection()
unsere Blog-Beiträge in die Variable posts
. Anschließend verwenden wir die map()
-Funktion, um die Metadaten jedes Beitrags (Titel, Beschreibung, Datum, Tags, …) an eine eigene Komponente <Card>
weiterzureichen. Die <Card>
-Komponente rendert daraus einen schönen Block mit Titel und Beschreibung des Beitrags:
<Layout pageTitle={pageTitle}>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
{posts.map((post) => (
<Card
href={`/blog/${post.id}/`}
title={post.data.title}
description={post.data.description}
date={post.data.updatedDate || post.data.pubDate}
tags={post.data.tags}
imagePath={`/src/images/hero/${post.data.heroImage}.jpg`}
/>
))}
</div>
</Layout>
Jeder <Card>
-Block wird mit einem Link (href={`/blog/${post.id}/`}
) auf den Artikel gerendert. Diese Blog-Artikelseite müssen wir jedoch erst einmal erstellen:
Blog-Artikelseite
Um für jeden Artikel in unserer Blog Content Collection eine Route zu rendern müssen wir mit getStaticPaths()
& render()
arbeiten und unter /pages/blog
eine Datei [...slug].astro
anlegen. Die slug
wird beim Rendern durch die id
des Beitrags ersetzt.
Der Vorgang wird in der Dokumentation (Building for static output (default)) im Detail beschrieben. Hier das Beispiel für unsere Blog Collection:
---
import { type CollectionEntry, getCollection, render } from "astro:content";
import BlogPost from "../../layouts/BlogPost.astro";
export async function getStaticPaths() {
const posts = await getCollection("blog");
return posts.map((post) => ({
params: { slug: post.id },
props: post,
}));
}
type Props = CollectionEntry<"blog">;
const post = Astro.props;
const { Content } = await render(post);
---
<BlogPost {...post.data}>
<Content />
</BlogPost>
Damit ist das Blog funktional einsatzbereit. Im nächsten Artikel kümmern wir uns um das Design:
Nächster Artikel
Weiter geht’s in Migration zum Astro Framework Teil 4 mit Tailwind CSS.