Statischen Build optimieren (Teil 4/7)
Teile 1-3 haben den Stack abgedeckt: Astro, Contentful und wie sie zusammenarbeiten. Die Seiten werden generiert, die Inhalte rendern. Aber zwischen „es funktioniert" und „es ist produktionsreif" liegt ein Unterschied.
Schließen wir diese Lücke mit Bildern, SEO, Sitemaps, RSS und einem Vorschau-Workflow.
Was der Build produziert
astro build erzeugt einen dist/-Ordner mit einfachen Dateien:
dist/
├── index.html
├── en/
│ ├── blog/
│ │ ├── index.html
│ │ └── hello-world/index.html
│ └── about/index.html
├── de/
│ └── ...
├── _astro/
│ ├── index.DK2a8b1c.css
│ └── hero.B3kf9d2e.webp
├── robots.txt
└── sitemap-index.xml
Jede Seite bekommt ihr eigenes Verzeichnis mit einer index.html. Saubere URLs wie /en/blog/hello-world funktionieren einfach so.
Bilder richtig machen
Für lokale Bilder erledigt Astro die Optimierung automatisch:
---
import { Image } from 'astro:assets'
import heroImage from '../assets/hero.jpg'
---
<Image
src={heroImage}
alt="Hero-Bereich"
widths={[400, 800, 1200]}
sizes="(max-width: 800px) 100vw, 1200px"
format="webp"
/>
Es generiert mehrere Größen in WebP und gibt ein <picture> mit srcset aus. Das 2MB-JPEG? Erledigt.
Für Contentful-Bilder ist es noch einfacher — deren CDN macht die Arbeit über URL-Parameter:
---
function contentfulImage(url: string, width: number, format = 'webp') {
return `https:${url}?w=${width}&fm=${format}&q=80`
}
const imageUrl = post.fields.featuredImage?.fields.file.url
---
{imageUrl && (
<picture>
<source
srcset={`
${contentfulImage(imageUrl, 400)} 400w,
${contentfulImage(imageUrl, 800)} 800w,
${contentfulImage(imageUrl, 1200)} 1200w
`}
sizes="(max-width: 800px) 100vw, 1200px"
type="image/webp"
/>
<img
src={contentfulImage(imageUrl, 800, 'jpg')}
alt={post.fields.featuredImage.fields.title}
loading="lazy"
/>
</picture>
)}
Keine Bildverarbeitung beim Build nötig. Einfach URL-Parameter. Nice.
SEO das funktioniert
Etwas, das meine aktuelle Hugo-Seite nicht hat: vernünftige Open-Graph-Metadaten pro Seite. Das sollte von Anfang an stimmen:
---
// src/components/SEO.astro
interface Props {
title: string
description?: string
image?: string
lang?: string
}
const {
title,
description = "Engineering Manager, Software Engineer, Tech Coach",
image,
lang = 'en'
} = Astro.props
const siteTitle = 'Oltion Zefi'
const fullTitle = title === siteTitle ? title : `${title} | ${siteTitle}`
---
<title>{fullTitle}</title>
<meta name="description" content={description} />
<link rel="canonical" href={Astro.url.href} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:url" content={Astro.url.href} />
{image && <meta property="og:image" content={image} />}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
{image && <meta name="twitter:image" content={image} />}
In jedes Layout einbauen und jede Seite bekommt richtige Social-Sharing-Tags:
---
// src/layouts/Base.astro
import SEO from '../components/SEO.astro'
const { title, description, image, lang } = Astro.props
---
<!DOCTYPE html>
<html lang={lang || 'en'}>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<SEO title={title} description={description} image={image} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</head>
<body>
<slot />
</body>
</html>
Für Blogbeiträge wird der Textauszug zur Beschreibung:
<Layout
title={post.fields.title}
description={post.fields.excerpt}
image={post.fields.featuredImage
? `https:${post.fields.featuredImage.fields.file.url}?w=1200&fm=jpg`
: undefined}
>
Sitemap
Ein Befehl:
npx astro add sitemap
Konfiguration:
import { defineConfig } from 'astro/config'
import sitemap from '@astrojs/sitemap'
export default defineConfig({
output: 'static',
site: 'https://oltionzefi.com',
integrations: [
sitemap({
i18n: {
defaultLocale: 'en',
locales: { en: 'en', de: 'de' }
}
})
]
})
Plus eine robots.txt in public/:
User-agent: *
Allow: /
Sitemap: https://oltionzefi.com/sitemap-index.xml
RSS-Feed
Etwas, das ich schon länger hinzufügen wollte:
npm install @astrojs/rss
// src/pages/rss.xml.ts
import rss from '@astrojs/rss'
import { getBlogPosts } from '../lib/contentful'
export async function GET(context) {
const posts = await getBlogPosts()
return rss({
title: 'Oltion Zefi - Blog',
description: 'Meine neuesten Beiträge und Artikel',
site: context.site,
items: posts.map(post => ({
title: post.fields.title,
pubDate: new Date(post.fields.date),
description: post.fields.excerpt,
link: `/en/blog/${post.fields.slug}/`
}))
})
}
Das war’s. RSS fertig.
Entwürfe vorab ansehen
Ein einfacher Workflow — Preview-Flag umschalten und unveröffentlichte Inhalte sehen:
{
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "CONTENTFUL_PREVIEW=true astro dev"
}
}
npm run preview ausführen, Entwürfe lokal sehen. Für eine persönliche Seite reicht das völlig.
Schnell halten
Ein paar Tipps für kurze Build-Zeiten:
Alle Posts einmal abrufen und als Props übergeben — keine einzelnen API-Aufrufe pro Seite:
---
export async function getStaticPaths() {
const posts = await getBlogPosts()
return posts.map(post => ({
params: { slug: post.fields.slug },
props: { post } // kein zweiter Abruf nötig
}))
}
---
Und astro check vor dem Build ausführen, um Probleme früh zu erkennen.
Der Output
Nach all dem sollte der Build etwa 48 HTML-Dateien und eine einzige CSS-Datei produzieren. Keine JavaScript-Bundles. PageSpeed liebt das.
Was kommt als Nächstes
Der Build ist schlank und produktionsreif. In Teil 5 packen wir alles in einen Docker-Container mit Nginx und richten automatische Deployments ein.
Weiter voran und genieße jeden Schritt deiner Coding-Reise.
