Zum Inhalt springen
CASOON

Programmatic SEO mit Gemini

Strukturierte Daten, Templates und das Modell – wie skalierbare Seiten entstehen, die trotzdem ranken

Aktualisiert 3. April 2026
12 Minuten
Programmatic SEO mit Gemini
#Programmatic SEO #Gemini #Astro #Next.js

Programmatic SEO funktioniert, wenn drei Dinge zusammenpassen: ein solides Template, strukturierte Daten und ein Modell, das daraus menschlich lesbare Inhalte macht. Dieser Artikel zeigt, wie Gemini in diesen Prozess eingebaut wird – von der Datenaufbereitung über das Prompt-Template bis zur automatischen Seitengeneration, die sich in bestehende Astro- oder Next.js-Projekte einfügt. Skalierbares Ranking ohne Qualitätsverlust ist kein Widerspruch, wenn der Prozess sauber aufgebaut ist.

Was Programmatic SEO ist und wann es funktioniert

Programmatic SEO bezeichnet die automatisierte Erstellung von Seiten auf Basis strukturierter Daten. Statt jede Seite einzeln zu schreiben, definiert man einmal ein Template und generiert daraus hunderte oder tausende Seiten – je eine pro Datensatz.

Das funktioniert hervorragend bei Themen mit klarer Struktur und hohem Wiederholungspotenzial:

  • Lokale Landingpages (“Klempner in [Stadt]”)
  • Produkt-Vergleichsseiten (“[Produkt A] vs. [Produkt B]”)
  • Kategorie- und Filter-Kombinationen für E-Commerce
  • FAQ-Seiten auf Basis von Datensätzen
  • Stellenbörsen, Immobilienportale, Rezeptseiten

Google wertet diese Inhalte als Spam, wenn sie inhaltlich identisch sind und keinen echten Mehrwert bieten. Der entscheidende Unterschied liegt darin, ob jede generierte Seite einzigartigen, nützlichen Content hat – oder ob nur der Stadtname ausgetauscht wird.

Genau hier kommt Gemini ins Spiel: Das Modell verwandelt strukturierte Daten in natürlich klingende, einzigartige Beschreibungen.

Die drei Bausteine

Programmatic SEO mit Gemini basiert auf drei klar getrennten Komponenten.

1
Strukturierte Daten CSV oder JSON mit den variablen Inhalten: Städtenamen, Produktdaten, Kategorien, Fakten. Diese Daten bestimmen, was einzigartig ist pro Seite.
2
Prompt-Template System Instruction mit Ton und Einschränkungen, User-Prompt mit Platzhaltern für die Variablen aus den strukturierten Daten.
3
Gemini API Pro Datensatz einen API-Aufruf – Gemini generiert einzigartigen Text aus den variablen Daten und dem fixen Template.
4
Astro / Next.js Build Generierter Content wird als MDX- oder JSON-Datei gespeichert. Das Framework rendert daraus statische HTML-Seiten.

Die Trennung ist wichtig: Strukturierte Daten, Template und Build-Prozess ändern sich unabhängig voneinander. Neue Städte hinzufügen bedeutet, die CSV zu erweitern – nicht das Template oder den Build-Prozess anzufassen.

Daten vorbereiten

Welches Datenformat ist besser, CSV oder JSON? JSON ist zu bevorzugen, weil es hierarchische Daten sauber abbildet und direkt in JavaScript-Builds eingebunden werden kann.

Beispiel: Städte-Daten für lokale Handwerker-Landingpages

[
  {
    "slug": "muenchen",
    "city": "München",
    "state": "Bayern",
    "population": 1512491,
    "districts": ["Schwabing", "Maxvorstadt", "Neuhausen", "Bogenhausen"],
    "landmarks": ["Marienplatz", "Englischer Garten", "Olympiapark"],
    "region_description": "Landeshauptstadt mit hoher Bevölkerungsdichte und stark nachgefragtem Handwerkermarkt",
    "avg_response_time_hours": 2.5,
    "coverage_radius_km": 30
  },
  {
    "slug": "nuernberg",
    "city": "Nürnberg",
    "state": "Bayern",
    "population": 515543,
    "districts": ["Altstadt", "Gostenhof", "Langwasser", "Gibitzenhof"],
    "landmarks": ["Kaiserburg", "Hauptmarkt", "Christkindlesmarkt"],
    "region_description": "Zweitgrößte Stadt Bayerns, starke Industrie- und Dienstleistungsregion",
    "avg_response_time_hours": 1.8,
    "coverage_radius_km": 25
  }
]

Die Spalten, die sich pro Datensatz unterscheiden, werden zu Platzhaltern im Prompt-Template. Je mehr einzigartige Daten pro Datensatz vorliegen, desto einzigartiger wird der generierte Content.

Das Prompt-Template aufbauen

Das Template ist der Kern des Systems. Es enthält die fixen Bestandteile (Ton, Format, Einschränkungen) und die variablen Platzhalter.

# System Instruction

Du bist Texter für einen regionalen Handwerksbetrieb.
Schreibe natürliche, informative Texte für lokale Landingpages.

Regeln:
- Kein Marketing-Sprech ("die besten", "günstigsten", "kompetentesten")
- Konkrete Fakten aus den gelieferten Daten verwenden
- Ortsbezüge müssen natürlich wirken, nicht aufgesetzt
- Tonalität: sachlich, vertrauenswürdig, lokal verankert
- Keine Wiederholungen innerhalb des Textes

# User Prompt Template

Erstelle eine Landingpage-Beschreibung für:

Stadt: {{city}}, {{state}}
Einwohner: {{population}}
Stadtteile: {{districts | join(", ")}}
Bekannte Orte: {{landmarks | join(", ")}}
Regionale Besonderheit: {{region_description}}
Reaktionszeit in der Region: {{avg_response_time_hours}} Stunden
Versorgungsradius: {{coverage_radius_km}} km

Schreibe:
1. Intro-Absatz (3-4 Sätze): Warum dieser Betrieb in {{city}} tätig ist, mit Bezug auf die Stadt
2. Leistungs-Absatz (3-4 Sätze): Konkrete Verfügbarkeit und Reaktionszeit
3. Stadtteile-Absatz (2-3 Sätze): Abgedeckte Stadtteile natürlich einweben

Kein JSON. Nur die drei Absätze als Fließtext, getrennt durch Leerzeilen.

Das Template mit Platzhaltern in doppelten geschweiften Klammern ist absichtlich einfach gehalten. In Python werden diese Platzhalter vor dem API-Aufruf durch die echten Daten ersetzt.

Integration in Astro

Astro mit Content Collections ist die sauberste Lösung für programmatisch generierten Content. Das Skript liest die JSON-Daten, ruft Gemini auf und schreibt MDX-Dateien.

Skript: scripts/generate-city-pages.ts

import * as fs from "fs";
import * as path from "path";
import { GoogleGenerativeAI } from "@google/generative-ai";

const genai = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);

const SYSTEM_INSTRUCTION = `Du bist Texter für einen regionalen Handwerksbetrieb.
Schreibe natürliche, informative Texte für lokale Landingpages.
Kein Marketing-Sprech. Konkrete Fakten. Sachlich und vertrauenswürdig.`;

interface CityData {
  slug: string;
  city: string;
  state: string;
  population: number;
  districts: string[];
  landmarks: string[];
  region_description: string;
  avg_response_time_hours: number;
  coverage_radius_km: number;
}

async function generateCityContent(city: CityData): Promise<string> {
  const model = genai.getGenerativeModel({
    model: "gemini-2.5-pro",
    systemInstruction: SYSTEM_INSTRUCTION,
    generationConfig: { temperature: 0.4 },
  });

  const prompt = `
Erstelle eine Landingpage-Beschreibung für:

Stadt: ${city.city}, ${city.state}
Einwohner: ${city.population.toLocaleString("de-DE")}
Stadtteile: ${city.districts.join(", ")}
Bekannte Orte: ${city.landmarks.join(", ")}
Regionale Besonderheit: ${city.region_description}
Reaktionszeit in der Region: ${city.avg_response_time_hours} Stunden
Versorgungsradius: ${city.coverage_radius_km} km

Schreibe drei Absätze: Intro (3-4 Sätze), Verfügbarkeit (3-4 Sätze), Stadtteile (2-3 Sätze).
Nur die drei Absätze als Fließtext, getrennt durch Leerzeilen.`;

  const result = await model.generateContent(prompt);
  return result.response.text();
}

function buildMDX(city: CityData, content: string): string {
  return `---
title: 'Klempner in ${city.city}'
description: 'Professioneller Klempnerservice in ${city.city} und Umgebung. Reaktionszeit ca. ${city.avg_response_time_hours} Stunden.'
city: '${city.city}'
slug: '${city.slug}'
draft: false
---

${content}
`;
}

async function main() {
  const citiesData: CityData[] = JSON.parse(
    fs.readFileSync("data/cities.json", "utf-8")
  );

  const outputDir = "src/content/cities";
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }

  for (const city of citiesData) {
    const outputPath = path.join(outputDir, `${city.slug}.mdx`);

    // Bereits generierte Dateien überspringen
    if (fs.existsSync(outputPath)) {
      console.log(`Überspringe ${city.city} (bereits vorhanden)`);
      continue;
    }

    console.log(`Generiere Seite für ${city.city}...`);
    const content = await generateCityContent(city);
    const mdx = buildMDX(city, content);

    fs.writeFileSync(outputPath, mdx, "utf-8");
    console.log(`Erstellt: ${outputPath}`);

    // Rate Limiting: 1 Sekunde Pause zwischen API-Aufrufen
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }

  console.log("Generierung abgeschlossen.");
}

main().catch(console.error);

Das Skript ausführen mit:

npx tsx scripts/generate-city-pages.ts

Anschließend normal bauen:

astro build

Integration in Next.js

In Next.js funktioniert der Ansatz ähnlich, nutzt aber getStaticPaths und getStaticProps. Hier ein kompakter Überblick:

// pages/klempner/[city].tsx
import { GetStaticPaths, GetStaticProps } from "next";
import citiesData from "../../data/cities.json";

export const getStaticPaths: GetStaticPaths = async () => {
  const paths = citiesData.map((city) => ({
    params: { city: city.slug },
  }));
  return { paths, fallback: false };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const city = citiesData.find((c) => c.slug === params?.city);
  // Content wurde vorab generiert und in data/generated-content.json gespeichert
  const generatedContent = require("../../data/generated-content.json");
  const content = generatedContent[city!.slug];

  return { props: { city, content } };
};

Der Unterschied zur Astro-Lösung: In Next.js empfiehlt sich, den generierten Content in einer JSON-Datei zu bündeln statt einzelne MDX-Dateien zu schreiben. Das Script-Muster für die Generierung ist identisch.

Qualitätskontrolle bei Scale

Bei hundert generierten Seiten manuell zu prüfen ist nicht realistisch. Automatisierung braucht automatische Qualitätssicherung.

interface QualityResult {
  passed: boolean;
  issues: string[];
  wordCount: number;
}

function checkQuality(
  content: string,
  city: string,
  minWords = 200
): QualityResult {
  const issues: string[] = [];
  const words = content.split(/\s+/).filter((w) => w.length > 0);
  const wordCount = words.length;

  // Mindestlänge
  if (wordCount < minWords) {
    issues.push(`Zu kurz: ${wordCount} Wörter (Minimum: ${minWords})`);
  }

  // Stadt muss vorkommen
  const cityMentions = (content.match(new RegExp(city, "gi")) || []).length;
  if (cityMentions < 2) {
    issues.push(`Stadt '${city}' nur ${cityMentions}x erwähnt`);
  }

  // Keine identischen Sätze aus dem Template
  const templatePhrases = ["Schreibe drei Absätze", "Erstelle eine Landingpage"];
  for (const phrase of templatePhrases) {
    if (content.includes(phrase)) {
      issues.push(`Template-Text im Output gefunden: "${phrase}"`);
    }
  }

  // Mindestanzahl Absätze
  const paragraphs = content.split(/\n\n+/).filter((p) => p.trim().length > 50);
  if (paragraphs.length < 3) {
    issues.push(`Zu wenige Absätze: ${paragraphs.length} (Minimum: 3)`);
  }

  return {
    passed: issues.length === 0,
    issues,
    wordCount,
  };
}

Was funktioniert, was nicht

Nicht jedes Thema eignet sich für programmatisches Vorgehen.

Use CaseEignungBesonderheit
Lokale LandingpagesSehr gutJe mehr Lokal-Daten, desto besser
Produkt-VergleichsseitenGutStrukturierte Produktdaten nötig
E-Commerce KategorieseitenGutDaten aus Produktkatalog nutzen
RezeptseitenGutStrukturierte Zutaten- und Nährwertdaten
FAQ aus DatensatzGutFrage-Antwort-Paare als JSON
NachrichtenartikelSchlechtKein Aktualitätsbezug möglich
Medizinische RatgeberNicht empfohlenYMYL, E-E-A-T kritisch
FinanzberatungNicht empfohlenYMYL, rechtliche Risiken
MeinungsbeiträgeNicht sinnvollPersönliche Stimme fehlt

Die Faustregel: Wenn der Use Case klar strukturierte, verifizierbare Daten hat und der Nutzer eine informational oder commercial Suchabsicht hat, funktioniert Programmatic SEO. Wenn der Use Case persönliche Expertise, aktuelle Ereignisse oder medizinische Verantwortung erfordert, nicht.

Nicht alles sollte indexiert werden

Ein häufiger Fehler bei Programmatic SEO ist nicht die Generierung, sondern die Veröffentlichung. Nur weil tausend Seiten technisch erzeugt werden können, heißt das nicht, dass tausend Seiten in den Index gehören.

Nicht indexiert werden sollten Seiten dann, wenn sie zu wenig Datentiefe haben, sich inhaltlich nur minimal unterscheiden oder keinen klaren Suchnutzen stiften. Genau dort entsteht das, was später wie Duplicate Content oder “thin content” aussieht – auch wenn die Texte formal unterschiedlich sind.

Praktisch heißt das:

  • Seiten mit sehr dünner Datenbasis besser noindex
  • ähnliche Varianten lieber zusammenfassen statt vervielfachen
  • erst die stärksten Cluster veröffentlichen, nicht sofort alles
  • Indexierung als redaktionelle Entscheidung behandeln, nicht als Default

Die bessere Strategie ist fast immer: weniger Seiten, dafür mit tieferem Datensatz und klarerem Nutzen.

Einordnung

Programmatic SEO mit Gemini ist kein Ersatz für redaktionellen Content. Es ist ein Werkzeug für spezifische, skalierbare Anwendungsfälle – und es funktioniert gut, wenn der Prozess sauber ist.

Der entscheidende Faktor ist die Datenqualität. Ein Template kann nur so gut sein wie die Daten, die es befüllen. Wer in die Aufbereitung der strukturierten Daten investiert, bekommt Content, der rankt. Wer mit dünnen Daten arbeitet, bekommt dünnen Content.

Der nächste Artikel in dieser Serie wechselt die Perspektive: von AI Studio und der Gemini API hin zu Vertex AI – Googles Enterprise-Plattform für KI. Was der Wechsel bedeutet, wann er sinnvoll ist und was er kostet.