Tippe, um Seiten und Abschnitte zu finden.

Zum Inhalt
Ginko

SEO und Agenten

Was Ginko für Suchmaschinen und KI-Agenten tut. Head-Tags, JSON-LD, Sitemap, robots.txt, Weiterleitungen, llms.txt, Markdown-Seiten, der MCP-Zugang der Doku und die Suche für deine eigene Seite.

Was du ohne Code bekommst

Das Nuxt-Modul liefert diese Adressen für jede Seite. Du schreibst keinen Code dafür:

AdresseInhalt
/sitemap.xmlJede Seite und jeder Eintrag mit Adresse, in jeder Sprache
/robots.txtRegeln für Crawler, mit der Sitemap
/llms.txtEin Verzeichnis der Seite für KI-Agenten
/llms-full.txtAlle Einträge der Hauptsprache als Markdown in einer Datei
<seite>.mdEin Eintrag als Markdown

Eine gleichnamige Datei in public/ gewinnt: deine eigene public/robots.txt oder public/llms.txt ersetzt die von Ginko.

Head-Tags und JSON-LD

Ruf useGinkoSeo einmal pro Seite auf. Es setzt Titel, Beschreibung, Open-Graph- und Twitter-Tags und JSON-LD. ginko-i18n ergänzt die kanonische URL, hreflang mit x-default und <html lang>. Die Optionen stehen auf Seiten lesen.

Die Vorgaben für die ganze Seite stehen in seo in ginko.config.ts:

ts
// ginko.config.ts
import { defineSite } from '@lupinum/ginko'

export default defineSite({
  site: 'linde',
  name: 'Gasthaus zur Linde',
  url: 'https://linde.example',
  timeZone: 'Europe/Vienna',
  locales: ['de'],
  currency: 'EUR',
  pages: { home: '/' },
  bricks: {},
  seo: {
    titleTemplate: '%s · Gasthaus zur Linde', // %s ist der Titel der Seite
    description: 'Landgasthaus mit Garten.',
    color: '#1e4032',                        // Markenfarbe der erzeugten Vorschaukarten
    image: '/og.png',                        // Vorschaubild ohne nuxt-og-image
    organization: { logo: '/logo.png', sameAs: ['https://www.instagram.com/linde'] },
  },
})

Ein Titel, der den Namen der Seite schon enthält, braucht die Vorlage nicht: useGinkoSeo({ title: 'Gasthaus zur Linde in Hallstatt', titleTemplate: false }) setzt ihn so, wie er ist.

Die Startseite bekommt JSON-LD vom Typ WebSite, mit seo.organization auch Organization. Jede andere Art gibst du selbst mit jsonLd mit, zum Beispiel eine Veranstaltung:

ts
const route = useRoute()
const event = useGinkoEntry('events', () => route.params.slug)
useGinkoSeo(() => ({
  title: event.value.data.title,
  type: 'article',
  jsonLd: [{
    '@context': 'https://schema.org',
    '@type': 'Event',
    name: event.value.data.title,
    startDate: event.value.data.date,
  }],
}))

Eine Brotkrumenspur baut breadcrumbJsonLd aus @lupinum/ginko: ein BreadcrumbList aus Schritten mit title und path, von der Startseite bis zur Seite selbst. Die Knoten von useGinkoBreadcrumbs passen direkt; die Adresse der Site macht die Pfade absolut:

ts
import { breadcrumbJsonLd } from '@lupinum/ginko'

const { url } = useGinkoSite()
const crumbs = useGinkoBreadcrumbs(['pages'])
useGinkoSeo(() => ({
  title: page.value.data.title,
  jsonLd: [breadcrumbJsonLd(crumbs.value, url)],
}))

Das Preset @lupinum/ginko-preset-gastro bringt JSON-LD für Restaurants, Speisekarten, Öffnungszeiten und Veranstaltungen mit (siehe Presets).

Sitemap

/sitemap.xml nennt jede Seite aus pages und jeden sichtbaren Eintrag mit Adresse, in jeder Sprache. Jede Adresse nennt ihre Übersetzungen (hreflang, mit x-default). Einträge tragen lastmod von ihrer letzten Änderung und bis zu 20 Bilder.

Eine Seite, die nicht in die Suche soll, zum Beispiel die Danke-Seite nach einem Formular, nennst du in seo.noindex mit ihrem Schlüssel aus pages. Sie fehlt dann in der Sitemap und antwortet in jeder Sprache mit X-Robots-Tag: noindex:

ginko.config.ts
pages: { thanks: { de: '/danke', en: '/en/thanks' } },
seo: { noindex: ['thanks'] },

Mit @nuxtjs/sitemap (allein oder über @nuxtjs/seo) gehört /sitemap.xml diesem Modul. Ginko gibt ihm dieselbe Liste als Quelle (/__ginko/sitemap.json) und schaltet seine Seitensuche aus (excludeAppSources: ['nuxt:pages']), die sonst Ordner auflisten würde, die keine Seiten sind. Deine eigenen Quellen in sitemap kommen zu denen von Ginko dazu.

robots.txt

Unter der eigenen Adresse der Seite (url in ginko.config.ts) erlaubt /robots.txt alles und nennt die Sitemap. www. zählt als dieselbe Adresse. Auf jedem anderen Host, zum Beispiel einer Vorschau, Staging oder localhost, verbietet sie alles, und jede Antwort trägt X-Robots-Tag: noindex. So landet eine Vorschau nie in einer Suchmaschine. Mit @nuxtjs/robots liefert dieses Modul /robots.txt, und dieselbe Regel gilt: Ginko markiert jeden anderen Host für das Modul als nicht indexierbar.

Nuxt SEO

Ginko braucht keines der Nuxt-SEO-Module. Eine Site darf sie trotzdem installieren (Nuxt 4.6.1 oder neuer), alle auf einmal mit @nuxtjs/seo oder einzeln, in beliebiger Reihenfolge in modules. Dann überlässt Ginko jedem Modul seine Ausgabe und gibt ihm die Daten der Site:

ModulWas Ginko ihm gibt
nuxt-site-configAdresse, Name, Beschreibung und Hauptsprache. Pro Anfrage: die Sprache der Seite und „nicht indexierbar“ auf jedem Host außer url
@nuxtjs/sitemapJede Seite und jeden Eintrag mit Übersetzungen, lastmod und Bildern (siehe Sitemap)
@nuxtjs/robotsDie Sitemap; Vorschau-Hosts bleiben gesperrt (siehe robots.txt)
nuxt-schema-orgDas JSON-LD der Seite kommt in den Graphen des Moduls (einmal gelesen, wenn useGinkoSeo läuft), und seo.organization wird die Identität. Das Modul ergänzt WebSite und WebPage
nuxt-seo-utilsTitel, kanonische URL, og:url und <html lang> von Ginko gehen den Vorgaben des Moduls vor, so steht nichts doppelt da
nuxt-og-imageDie Vorschaukarte von Ginko (siehe Seiten lesen)
nuxt-ai-readyAus: Ginko liefert /llms.txt und die Markdown-Seiten selbst. Mit agents: { enabled: false } bleibt das Modul an

Deine eigenen Einstellungen in nuxt.config.ts gehen allem vor, was Ginko übergibt. Eine Liste, die du selbst setzt, etwa sitemap.excludeAppSources oder robots.sitemap, ersetzt die von Ginko; sitemap.sources kommt zur Quelle von Ginko dazu. Ein Modul, das du ausschaltest (sitemap: false), bleibt aus.

Weiterleitungen

Eine verschobene Seite antwortet mit einer Weiterleitung, ohne Angabe 301. Es gibt drei Quellen:

  • Ein Eintrag nennt seine alten Adressen in redirectFrom. Siehe Inhalte aus Dateien.
  • Auf der Plattform ändert der Inhaber oder die Inhaberin die Adresse einer Seite im Studio („Ändern“ neben der Adresse). War die Seite schon online, leitet die alte Adresse auf die neue weiter und bleibt für andere Einträge gesperrt.
  • Die Seite listet Regeln in redirects:
ts
// ginko.config.ts
export default defineSite({
  // …
  redirects: [
    { from: '/menu', to: '/speisekarte' },
    { from: '/events/:slug', to: '/veranstaltungen/:slug' },
    { from: '/alter-blog/*', to: '/blog/:splat', status: 302 },
  ],
})

:name passt auf einen Abschnitt des Pfads. Ein * am Ende passt auf den Rest, den to als :splat nutzt. Die erste passende Regel gewinnt, und der Query-String bleibt erhalten. Eine ungültige Regel stoppt den Build.

Text für KI-Agenten

Agenten lesen Markdown besser als HTML. Darum gibt es jeden Eintrag einer Sammlung mit route auch als Markdown:

  • <seite>.md: /docs/sprachen.md liefert die Seite /docs/sprachen als Markdown.
  • Nur die .md-Adresse antwortet mit Markdown. Die normale Adresse einer Seite antwortet immer mit HTML, egal was ihr Accept-Header verlangt: Ein Host-Cache wie der von Vercel hält eine Fassung pro Adresse, sonst bekäme nach einer einzigen Markdown-Anfrage jeder Besucher Markdown.
  • Link-Header: Seiten von Datei-Einträgen nennen ihre Markdown-Fassung (rel="alternate"), damit ein Agent sie findet.
  • Nicht gefunden: Eine .md-Adresse ohne Eintrag antwortet mit 404 in Markdown und schlägt ähnliche Seiten vor.
  • Ein Punkt im Slug (/blog/v1.2) ändert nichts: Das Markdown liegt unter /blog/v1.2.md, und der Link-Header geht wie bei jeder Seite.

/llms.txt listet die Einträge der Hauptsprache mit Links auf ihr Markdown. Unter dem Namen der Seite steht agents.description, sonst seo.description. /llms-full.txt enthält den Text all dieser Einträge in einer Datei.

ts
// ginko.config.ts
export default defineSite({
  // …
  agents: { description: 'Öffnungszeiten, Speisekarte und Veranstaltungen des Gasthauses zur Linde.' },
})

agents: { enabled: false } schaltet alles davon ab: /llms.txt, /llms-full.txt und die Markdown-Fassungen.

agents: { review: true } lässt jeden Agenten, der über die Plattform schreibt, einen Vorschlag schreiben, der ein Ja von Inhaber oder Admin braucht (siehe Plattform und CLI).

Der MCP-Zugang der Doku

Eine Doku-Seite mit @lupinum/ginko-docs antwortet auch unter POST /__ginko/docs-mcp. Das ist ein MCP-Server (Streamable HTTP) mit zwei Werkzeugen, die nur lesen:

  • search_docs(query, locale?): die besten Treffer unter Seiten und Abschnitten, mit Pfad und Auszug,
  • read_doc(path): eine Seite als Markdown.

Ein Agent verbindet sich über die Adresse, zum Beispiel in Claude Code:

Terminal
claude mcp add --transport http meine-doku https://docs.example.com/__ginko/docs-mcp

Der Zugang braucht keinen Schlüssel und zeigt nur, was die öffentliche Seite zeigt. Er braucht einen Server: Ein statischer Build (nuxt generate) hat keinen MCP-Zugang.

Adressen und Header

Agenten, Skripte und Crawler dürfen sich auf diese Adressen und Header verlassen. Bis 1.0 ändert eine stabile Namen oder Bedeutung nur in einer Minor-Version, und das Changelog nennt die Änderung.

Adresse oder HeaderWas es ist
<seite>.mdDer Eintrag als Markdown, Content-Type: text/markdown; charset=utf-8. Ohne Eintrag: 404, ebenfalls in Markdown.
Link: <seite.md>; rel="alternate"; type="text/markdown"Auf Seiten von Datei-Einträgen.
/llms.txt, /llms-full.txtDie Einträge der Hauptsprache: eine Liste mit Links und der ganze Text.
/sitemap.xml, /robots.txtSiehe Sitemap und robots.txt.
X-Robots-Tag: noindexAuf jedem Host außer url, auf jeder Adresse unter /__ginko/ und auf den Seiten aus seo.noindex.
/__ginko/search/<sprache>.jsonDer Suchindex (siehe unten). Sein Format ist SearchIndexJson aus @lupinum/ginko/search.
/__ginko/docs-mcpDer MCP-Zugang einer Doku-Seite.
/_ginko/img/<hash>.<endung>Bilder neben Inhaltsdateien, ein Jahr im Cache. Der Hash kommt aus dem Inhalt: Behandle ihn als undurchsichtig und nimm die Adresse aus dem Eintrag, bau sie nie selbst.

Alles andere ist intern und kann sich in jeder Version ändern: die übrigen Adressen unter /__ginko/ (etwa die Vorschau und /__ginko/files/) und die Header x-ginko-rev, x-ginko-min-rev und x-ginko-site-token. Die ganze Liste steht unter Intern, nicht verwenden.

Suche für deine eigene Seite

Ginko baut einen Suchindex pro Sprache: einen Datensatz für den Text vor der ersten ##-Überschrift eines Eintrags und einen für jeden ##- und ###-Abschnitt. Die Seite liefert ihn unter /__ginko/search/<sprache>.json. Hat die Seite nur Datei-Bausteine, baut ihn der Build. Hat sie Plattform-Bausteine, baut ihn der Server der Seite aus denselben Inhalten wie die Seiten, ohne abgelaufene Einträge, und er wird wie eine Seite neu gebaut: Was im Studio veröffentlicht ist, findet die Suche nach spätestens einer Minute. Die Doku-Ebene nutzt ihn für ihre ⌘K-Suche. In deiner eigenen Seite liest du ihn mit @lupinum/ginko/search:

vue
<!-- app/components/SiteSearch.vue -->
<script setup lang="ts">
import type { SearchHit, SearchIndex, SearchIndexJson } from '@lupinum/ginko/search'

const { locale } = useI18n()
const query = ref('')
const hits = ref<SearchHit[]>([])
const indexes = new Map<string, SearchIndex>()

watch(query, async (text) => {
  // Suchmaschine und Index erst laden, wenn jemand sucht.
  const { loadIndex, search } = await import('@lupinum/ginko/search')
  let index = indexes.get(locale.value)
  if (!index) {
    index = await loadIndex(await $fetch<SearchIndexJson>(`/__ginko/search/${locale.value}.json`))
    indexes.set(locale.value, index)
  }
  hits.value = search(index, text, { limit: 8 })
})
</script>

<template>
  <input v-model="query" type="search" aria-label="Suchen">
  <ul>
    <li v-for="hit in hits" :key="hit.id">
      <NuxtLink :to="hit.url">{{ hit.heading ?? hit.title }}</NuxtLink>
      <p>{{ hit.excerpt }}</p>
    </li>
  </ul>
</template>

search findet Wörter über ihren Anfang und verzeiht kleine Tippfehler. Ein Treffer hat id, url (mit dem #anker seines Abschnitts), title, heading, excerpt und score. Jeder Index folgt seiner Sprache. Deutsch, Englisch, Französisch, Italienisch, Spanisch, Niederländisch und Portugiesisch finden andere Formen eines Worts („Gerichte“ findet „Gericht“, „connecting“ findet „connected“). Andere Sprachen finden Wörter über ihren Anfang. Akzente spielen keine Rolle: „cafe“ findet „café“, und im Deutschen findet „ae“ das „ä“. Inhalte von der Plattform stehen nicht in diesem Index.