GraphQL-API & SDK
GraphQL-Lesezugriff auf die Delivery-Daten mit einem Schema, das aus den Inhaltstypen entsteht, der Schema-Endpunkt für Codegenerierung und das TypeScript-SDK @entrydeck/sdk für Nuxt, Next und Node — mit Beispielen auf Deutsch und Englisch.
Stand: 19.10.2026
Auf dieser Seite
Neben der REST-Delivery-API gibt es zwei weitere Wege zu den veröffentlichten Inhalten: eine GraphQL-API unter demselben Pfad und das SDK @entrydeck/sdk, das beide Wege in einem typisierten Client bündelt. Beide liefern dieselben Antwortformen wie REST — ein Frontend kann sie beliebig mischen.
GraphQL-Endpunkt
| Methode und Pfad | Zweck |
|---|---|
POST /api/v1/delivery/graphql |
GraphQL-Queries (nur lesend) |
GET /api/v1/delivery/schema |
Feldschemata aller aktiven Inhaltstypen als JSON — Grundlage für Codegenerierung |
GET /api/v1/delivery/schema?format=sdl |
das generierte GraphQL-Schema als SDL (Text) |
GET /api/v1/delivery/graphql |
GraphiQL-Playground — nur wenn GRAPHQL_PLAYGROUND=1 gesetzt ist |
Authentifizierung und Website-Kontext sind identisch mit REST: x-api-key (plus optional x-site-id) oder Plattform-Schlüssel mit x-site-host. Der Body ist das übliche GraphQL-JSON: { "query": "…", "variables": { … }, "operationName": "…" }.
curl https://entrydeck.app/api/v1/delivery/graphql \
-H "x-api-key: $ENTRYDECK_API_KEY" -H "content-type: application/json" \
-d '{"query":"{ site { name defaultLocale } navigation(handle: \"main\") { items { label href } } }"}'Das Schema entsteht aus den Inhaltstypen
Für jeden Inhaltstyp gibt es einen Objekttyp (Slug in PascalCase: beitrag → Beitrag, team-mitglied → TeamMitglied) und zwei Queries: beitrag(slug, locale, expand) für einen Eintrag und beitrags(locale, limit, offset, page, filter, sort, order, featured, expand) für die Liste. Der Plural folgt einfachen englischen Regeln und ist damit vorhersagbar. Jeder Objekttyp trägt die Basisfelder eines Eintrags (id, slug, locale, title, seo, data, createdAt, updatedAt, website) plus die Felder des Inhaltstyps, typisiert:
| Feldtyp | GraphQL |
|---|---|
| text, textarea, slug, email, phone, url, date, datetime, select, radio | String ([String] bei Mehrfachauswahl) |
| richtext | String (HTML) |
| number | Float |
| boolean | Boolean |
| list | [String] |
| media | Media { id url thumbnailUrl width height alt title mimeType focalX focalY variants … } |
| relation | der Zieltyp (relatedType) oder das Interface ContentEntry |
| json | JSON |
| blocks | JSON (Rohwert) plus <feld>Blocks: eine Union typisierter Blöcke, wenn der Inhaltstyp blocks: [{ type, label, fields }] definiert |
Feldnamen werden GraphQL-tauglich gemacht (preis-brutto → preisBrutto). Ein Datenfeld, das wie ein Basisfeld heißt (etwa title), bleibt über data erreichbar. Fehlt locale, gilt die Standardsprache der Website. Nicht gefundene Seiten oder Einträge liefern null statt eines Fehlers; Fehler des Dienstes tragen extensions.code und extensions.status (z. B. NOT_FOUND/404 für einen unbekannten Inhaltstyp).
Beispiel-Queries
query Startseite($locale: String) {
site { name defaultLocale theme }
page(path: "/", locale: $locale, expand: "all") {
page { title seo blocks { blockKey configJson } }
breadcrumbs { title fullPath }
}
navigation(handle: "main", locale: $locale) {
items { label href children { label href } }
}
}query Beitraege {
beitrags(limit: 10, sort: publishedAt, order: desc, filter: { kategorie: "news" }) {
total
items {
slug title
teaser
bild { url width height alt }
autor { title slug }
inhaltBlocks {
__typename
... on BeitragInhaltTextBlock { html }
... on BeitragInhaltHeroBlock { headline image { url } }
}
}
}
beitrag(slug: "hallo-welt") { title body data }
}{
search(q: "Ausbildung mit Sehbehinderung", limit: 5) { mode items { slug title excerpt score } }
sitemap { baseUrl pages { url updatedAt } content { url updatedAt } }
pagesTree { title fullPath children { title fullPath } }
contentTypes { slug graphqlType fields }
}Generische Queries für dynamische Frontends: entry(type: "beitrag", slug: "…") und entries(type: "beitrag", filter: { … }) liefern das Interface ContentEntry mit data als JSON.
Grenzen und Caching
- Tiefe höchstens 8 Ebenen, geschätzte Komplexität höchstens 2000, Timeout 10 s — einstellbar über
GRAPHQL_MAX_DEPTH,GRAPHQL_MAX_COMPLEXITY,GRAPHQL_TIMEOUT_MS. Zu tiefe oder zu breite Queries werden mit400undQUERY_TOO_DEEPbzw.QUERY_TOO_COMPLEXabgelehnt. - Introspektion (
__schema) ist nur mitGRAPHQL_PLAYGROUND=1erlaubt. Für Codegenerierung ohne Playground genügtGET /delivery/schema?format=sdl. - Das Schema wird je Mandant gecacht. Nach 60 Sekunden (
GRAPHQL_SCHEMA_TTL_MS) wird ein Hash der Inhaltstypen verglichen; nur bei Änderung wird neu gebaut. Die Antwort trägt den Hash im HeaderX-Schema-Hash. - Medien und Relationen werden je Request gebündelt geladen (keine N+1-Abfragen).
SDK @entrydeck/sdk
Ein TypeScript-Client ohne Abhängigkeiten außer fetch — für Node 18+, Next, Nuxt, Astro, SvelteKit und Worker. Der API-Key gehört auf den Server.
npm install @entrydeck/sdkimport { createClient, EntrydeckError } from '@entrydeck/sdk'
const cms = createClient({
baseUrl: 'https://entrydeck.app', // oder …/api/v1
apiKey: process.env.ENTRYDECK_API_KEY!,
siteId: process.env.ENTRYDECK_SITE_ID, // optional
locale: 'de', // Standardsprache aller Aufrufe
})
const site = await cms.site()
const home = await cms.page('/', { expand: 'all' })
const nav = await cms.navigation('main')
const news = await cms.entries<{ title: string; teaser?: string }>('beitrag', {
limit: 10, sort: 'publishedAt', order: 'desc', filter: { kategorie: 'news' },
})
const post = await cms.entry('beitrag', 'hallo-welt', { expand: 'media' })
const hits = await cms.search('Ausbildung', { limit: 5 })
const map = await cms.sitemap()
const ok = await cms.submitForm('kontakt', { email: 'a@b.de', message: 'Hallo' })
const { data } = await cms.graphql('{ site { name } }')| Methode | Endpunkt |
|---|---|
site() |
GET /delivery/site |
page(path, { locale?, expand? }) |
GET /delivery/page |
pagesTree() |
GET /delivery/pages/tree |
navigation(handle) |
GET /delivery/navigation/{handle} |
entries<T>(type, { limit, offset, page, filter, sort, order, featured, expand }) |
GET /delivery/content/{typ} |
entry<T>(type, slug, { expand? }) |
GET /delivery/content/{typ}/{slug} |
search(q, { limit?, mode? }) |
GET /delivery/search |
sitemap() |
GET /delivery/sitemap |
form(slug), submitForm(slug, data) |
GET /delivery/form/{slug}, POST /forms/{id}/submissions |
preview(token) |
GET /delivery/preview/page |
schema() |
GET /delivery/schema |
graphql<T>(query, variables?) |
POST /delivery/graphql |
Alle Methoden reichen fetch-Optionen unverändert durch — next: { revalidate: 60 }, cache: 'no-store', signal —, damit ISR und Framework-Caches ohne Umwege funktionieren. Fehler kommen als EntrydeckError mit status (0 = Netzwerk), code, body, graphqlErrors und bei abgelehnten Formularen fieldErrors.
Next.js (App Router, ISR)
// lib/cms.ts
export const cms = createClient({
baseUrl: process.env.ENTRYDECK_URL!,
apiKey: process.env.ENTRYDECK_API_KEY!,
locale: 'de',
requestInit: { next: { revalidate: 60, tags: ['cms'] } },
})
// app/[...slug]/page.tsx
export default async function Page({ params }: { params: { slug?: string[] } }) {
try {
const { page } = await cms.page('/' + (params.slug ?? []).join('/'), { expand: 'all' })
return <Blocks blocks={page.blocks} seo={page.seo} />
} catch (err) {
if (err instanceof EntrydeckError && err.isNotFound) notFound()
throw err
}
}Ein Webhook auf eine Route, die revalidateTag('cms') aufruft, erneuert die Seiten nach jeder Veröffentlichung.
Nuxt 3
// server/utils/cms.ts — der Key bleibt auf dem Server
export const cms = createClient({
baseUrl: useRuntimeConfig().entrydeckUrl,
apiKey: useRuntimeConfig().entrydeckApiKey,
locale: 'de',
})
// server/api/page.get.ts
export default defineEventHandler((event) => {
const { route = '/' } = getQuery(event)
return cms.page(String(route), { expand: 'all' })
})<script setup lang="ts">
const route = useRoute()
const { data } = await useFetch('/api/page', { query: { route: route.path } })
</script>Soll useFetch direkt gegen die API gehen, liefert cms.requestFor('delivery/page', { route: '/', locale: 'de' }) die passende url samt headers.
TypeScript-Typen aus den Inhaltstypen erzeugen
npx entrydeck-generate-types --base-url https://entrydeck.app --api-key $ENTRYDECK_API_KEY --out src/types/entrydeck.tsDas Skript liest GET /delivery/schema und schreibt je Inhaltstyp ein …Data-Interface, Entry<…>-Aliasse, Block-Interfaces für blocks-Felder und die Zuordnung EntrydeckTypes (Slug → Datentyp). Optionen: --site-id, --from schema.json (offline), --sdk <modul>, --no-literals.
import type { BeitragData, EntrydeckTypes } from './types/entrydeck'
const post = await cms.entry<BeitragData>('beitrag', 'hallo-welt')
post.data.title // string | null
post.data.bild // MediaRef — Id oder, mit expand=media, das MedienobjektEnglish summary
GraphQL. POST /api/v1/delivery/graphql accepts read-only GraphQL queries with the same authentication (x-api-key + optional x-site-id, or platform key + x-site-host) and the same site context as the REST Delivery API. The schema is generated per tenant from the content types: one object type per type (beitrag → Beitrag), the queries <type>(slug, locale, expand) and <typePlural>(locale, limit, offset, page, filter, sort, order, featured, expand), plus site, page(path, locale, expand), pagesTree(locale), navigation(handle, locale), search(q, locale, limit, mode), sitemap(locale), the generic entry/entries and contentTypes. Field types map to String, Float, Boolean, [String], Media, the related type (or the ContentEntry interface) and the JSON scalar; blocks fields additionally expose a typed <field>Blocks union. Responses carry the same fields as the REST endpoints, so both can be used interchangeably. Limits: depth 8, estimated complexity 2000, timeout 10 s (GRAPHQL_MAX_DEPTH, GRAPHQL_MAX_COMPLEXITY, GRAPHQL_TIMEOUT_MS). Introspection and the GraphiQL playground at GET /delivery/graphql are only available with GRAPHQL_PLAYGROUND=1; GET /delivery/schema returns the field schemas as JSON and, with ?format=sdl, the generated schema. The schema is cached per tenant with a 60 s TTL and rebuilt only when the hash of the content types changes (X-Schema-Hash header).
SDK. npm install @entrydeck/sdk, then createClient({ baseUrl, apiKey, siteId?, locale?, fetch?, requestInit? }) gives you site(), page(path), pagesTree(), navigation(handle), entries(type, { limit, offset, filter, sort, expand }), entry(type, slug), search(q), sitemap(), form(slug), submitForm(slug, data), preview(token), schema() and graphql(query, variables). Every call passes fetch options through (next: { revalidate }, cache, signal), errors are thrown as EntrydeckError with status, code, body, graphqlErrors and fieldErrors. npx entrydeck-generate-types --base-url … --api-key … --out types.ts generates TypeScript interfaces for your content types from GET /delivery/schema. See the package README for complete Next.js and Nuxt examples.