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:
| Adresse | Inhalt |
|---|---|
/sitemap.xml | Jede Seite und jeder Eintrag mit Adresse, in jeder Sprache |
/robots.txt | Regeln für Crawler, mit der Sitemap |
/llms.txt | Ein Verzeichnis der Seite für KI-Agenten |
/llms-full.txt | Alle Einträge der Hauptsprache als Markdown in einer Datei |
<seite>.md | Ein 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:
// 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:
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:
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:
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:
| Modul | Was Ginko ihm gibt |
|---|---|
nuxt-site-config | Adresse, Name, Beschreibung und Hauptsprache. Pro Anfrage: die Sprache der Seite und „nicht indexierbar“ auf jedem Host außer url |
@nuxtjs/sitemap | Jede Seite und jeden Eintrag mit Übersetzungen, lastmod und Bildern (siehe Sitemap) |
@nuxtjs/robots | Die Sitemap; Vorschau-Hosts bleiben gesperrt (siehe robots.txt) |
nuxt-schema-org | Das 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-utils | Titel, kanonische URL, og:url und <html lang> von Ginko gehen den Vorgaben des Moduls vor, so steht nichts doppelt da |
nuxt-og-image | Die Vorschaukarte von Ginko (siehe Seiten lesen) |
nuxt-ai-ready | Aus: 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:
// 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.mdliefert die Seite/docs/sprachenals Markdown.- Nur die
.md-Adresse antwortet mit Markdown. Die normale Adresse einer Seite antwortet immer mit HTML, egal was ihrAccept-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 derLink-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.
// 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:
claude mcp add --transport http meine-doku https://docs.example.com/__ginko/docs-mcpDer 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 Header | Was es ist |
|---|---|
<seite>.md | Der 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.txt | Die Einträge der Hauptsprache: eine Liste mit Links und der ganze Text. |
/sitemap.xml, /robots.txt | Siehe Sitemap und robots.txt. |
X-Robots-Tag: noindex | Auf jedem Host außer url, auf jeder Adresse unter /__ginko/ und auf den Seiten aus seo.noindex. |
/__ginko/search/<sprache>.json | Der Suchindex (siehe unten). Sein Format ist SearchIndexJson aus @lupinum/ginko/search. |
/__ginko/docs-mcp | Der 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:
<!-- 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.