|

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.