legrisch/statamic-enhanced-graphql
Statamic Enhanced GraphQL is a Statamic CMS addon that extends the built-in GraphQL API with additional querying capabilities, helping developers expose and retrieve content through more flexible, developer-friendly GraphQL endpoints for headless or decoupled sites.
Requirements and install
composer require legrisch/statamic-enhanced-graphql
Check composer.json in the package for its Statamic constraint before installing — the last release is July 2023, so verify it against your Statamic 4/5 install rather than assuming compatibility. Then clear everything:
php artisan optimize:clear
php artisan statamic:stache:clear # blueprint/collection schema is cached here
Wire up the endpoint first, the addon second. The package adds queries to Statamic's existing GraphQL schema, so nothing works until core GraphQL does:
config/statamic/graphql.php in your app) and that POST /graphql returns a schema error rather than a 404./graphql) and introspect. The package's queries appear as siblings of Statamic's built-in entries, entry, collection, globalSet, etc.Where to look first
src/ query classes — the package implements Statamic GraphQL query objects and registers them in its service provider. That service provider and one query class are the best template you'll get for your own custom queries.npm-free introspection dump) — the fastest way to see which arguments each enhanced query actually accepts.First use case
A headless frontend (Next/Nuxt/Inertia) that needs to fetch entries with filtering or sorting the stock entries query can't express — e.g. "published news posts in the blog collection, filtered on a custom topic field and a date range, sorted, paginated, in one round trip." Build that single query first; if the addon can't express it, you've learned that before you architected around it.
Sanity check the shape before building anything:
query SmokeTest {
entries(collection: "blog", limit: 1) {
data { id title slug }
}
}
Then swap in the enhanced query named in the README and diff the response shape against the built-in one.
Use it as an addition, not a replacement. The stock Statamic queries remain valid. Keep one canonical selection set per content type and reuse it via fragments across both the built-in and enhanced queries:
fragment EntryCard on Entry {
id
title
slug
url
date
... on Entry_Blog_BlogPost {
topic
hero_image { url }
}
}
query BlogIndex {
# built-in query — works fine where it fits
entries(collection: "blog", sort: "date:desc", limit: 12) {
data { ...EntryCard }
}
# enhanced query — where you need the extra filtering
enhancedEntries(collection: "blog", limit: 12) {
data { ...EntryCard }
}
}
The ... on Entry_<Collection>_<Blueprint> inline fragment is the piece people forget: fields are not on the base Entry type, so a flat selection silently fails or is rejected.
Pattern: move filtering server-side. The main win is expressing conditions the built-in filter argument (a JSON-ish string with field:operator:value entries) handles poorly — multi-field ORs, ranges, custom-field sorting. Build a small PHP-side QueryBuilder wrapper that maps your app's filter state to GraphQL variables rather than concatenating filter strings in JavaScript:
// app/GraphQL/EntryQuery.php
final class EntryQuery
{
public function __construct(
private string $collection,
private array $filters = [],
private string $sort = 'date:desc',
private int $limit = 12,
private int $page = 1,
) {}
public function toGraphQL(): array
{
return [
'query' => 'query ($collection: String!, $limit: Int, $page: Int) {
enhancedEntries(collection: $collection, limit: $limit, page: $page) {
data { ...EntryCard }
total
current_page
last_page
}
}',
'variables' => [
'collection' => $this->collection,
'limit' => $this->limit,
'page' => $this->page,
],
];
}
}
Push filters into the variables object too — never interpolate user input into the query string. (Variable names and accepted argument types must match the package's schema; introspect before assuming the above compiles.)
Pattern: global sets in one document. If the package adds a multi-globalset query, use it to hydrate your layout — footer, header nav, site settings — in one request instead of one query per set. Pair it with the collection queries in a single persisted query so the frontend has one document to cache.
Pattern: cache the resolved response, not the SQL. GraphQL is POST-only, so Statamic's static caching won't help you. Cache at the Laravel layer keyed on query + variables + site:
$key = 'gql:' . md5($query->toGraphQL()['query'] . serialize($variables) . $site);
return Cache::remember($key, now()->addMinutes(5), fn () => $client->post('/graphql', $payload));
Invalidate on entry save via Entry::saved / Entry::deleted listeners, or keep the TTL short. Statamic's stache makes the underlying reads cheap; the cache is mostly about network round trips.
Custom query extension. Add your own query in a service provider the same way the package does, then copy one of its classes as a template. This is the highest-leverage move with a 2-star package: anything too specific to your domain should live in your app, not in a dependency you can't patch upstream.
Pin it, and assume you'll fork it. Two stars, zero dependents, last release July 2023. That's not a knock on the code — it's a maintenance risk assessment. Require an exact version ("legrisch/statamic-enhanced-graphql": "1.0.0"), and if a core Statamic upgrade breaks it, plan on vendoring the src/ directory into app/GraphQL/ and adjusting the service provider rather than waiting for a release.
Argument shapes are not the same as the core queries. The enhanced queries exist because they take different (richer) arguments. The most common bug is copy-pasting a core entries selection onto an enhanced query, or assuming filter: "{...}" works identically. Introspect each query individually; don't infer from the built-in docs.
Multi-site defaults silently pick a site. Omitting site yields the default site's entry, and the response looks valid — you just get the wrong content. Always pass site explicitly, derived from the request locale, and add an assertion in your query wrapper that it's set.
Drafts, future-dated entries, and permissions. Verify the addon's queries respect the same visibility rules as core (status, published, current-user permissions). If they don't, you can leak unpublished content through a public endpoint. Test with an unpublished entry before shipping. Guard the /graphql route with your own middleware if the endpoint shouldn't be world-readable.
"Cannot query field" almost always means a stale schema. After changing a blueprint, adding a field, or toggling a collection, run:
php artisan statamic:stache:clear
php artisan optimize:clear
and reload GraphiQL. If the field is genuinely missing, check that the fieldtype is exposed to GraphQL at all — some addon fieldtypes aren't, and no query layer can fix that.
Depth and N+1. The enhanced filtering doesn't change how nested relationship fields resolve. A deeply nested query over a large collection will issue a query per relationship. Keep limit low, always paginate list fields, and profile with the Laravel query log (DB::listen) or Laravel Telescope during development rather than guessing.
Field name collisions. If the addon adds fields to shared types like Entry, a future Statamic release may add the same name and cause a schema build error. Keep a note of the type extensions in your service provider so a schema failure at boot points you at the right place.
Escape hatches to know: Statamic's type extension points (extending the entry/collection/global-set types with fields) and the query registry in the package's service provider. Between those two, anything the addon doesn't do, you can add in your own app without touching vendor code — which is exactly what you want from a package this small.
How can I help you explore Laravel packages today?