Freeze GraphQL query results into JSON files, next to the queries.
gqlfreez looks through a folder, finds every .graphql / .gql file, runs the query
against your endpoint, and writes the result to a .json file with the same name, right
next to it.
src/queries/posts.graphql → src/queries/posts.json
It runs before your build, and it works with any static site generator — Astro, Eleventy, Hugo, Jekyll, Zola — because it only reads and writes files. There is no plugin to install and nothing to set up in your generator.
Download a binary from the releases page — Linux and macOS on x64 and arm64, Windows on x64. Nothing else to install: no Node, no Rust, no libraries.
curl -fsSL https://github.com/lexoyo/gqlfreez/releases/latest/download/gqlfreez-linux-x64 \
-o /usr/local/bin/gqlfreez && chmod +x /usr/local/bin/gqlfreezOn macOS, replace linux-x64 with darwin-arm64 (Apple silicon) or darwin-x64 (Intel).
npx gqlfreezandcargo install gqlfreezdo not work yet — publishing to npm and crates.io is still on the roadmap. For now, download the binary.
# src/queries/posts.graphql
{ posts(first: 10) { nodes { title slug } } }gqlfreez ./src --endpoint https://example.com/graphql// src/queries/posts.json
{ "posts": { "nodes": [ { "title": "…", "slug": "…" } ] } }The file holds what was inside data, not the whole GraphQL response: it starts with
{ "posts": … }, not { "data": { "posts": … } }. Pass --envelope to keep the whole
response.
See examples/ for a working Eleventy site and a working Zola site, both
against a public API you can run right now.
A field that contains nodes or edges is a Relay connection. When you do not ask for
a limit, gqlfreez fetches every page and joins the results.
{ posts { nodes { title } } } # everything
{ posts(first: auto) { nodes { title } } } # everything, when `first` is required
{ posts(first: 100, after: $cursor) { … } } # everything, written out in full
{ posts(first: 20) { nodes { title } } } # twenty. A limit is a limit.Three ways to say "everything", because no single one works on every server:
- Leave
firstout. Works on WPGraphQL and most servers. first: auto. GitHub requiresfirstorlaston every connection, so leaving it out fails there.autonever leaves your machine:gqlfreezreplaces it with--page-sizebefore sending the query. The downside is that the schema expects anIntthere, so your editor will show an error.- Write
after: $cursoryourself. Works everywhere and keeps autocompletion, but it is longer.
pageInfo does not turn pagination on, it is only a field. If you did not ask for it,
gqlfreez adds it to paginate and removes it from the output. If you did ask for it, it is
kept and updated to describe the joined result — and hasNextPage comes from the last page
actually fetched, never set to false to look tidy.
Forward pagination only. Connections next to each other each get their own query. A
connection under a list — posts { nodes { comments { nodes } } } — cannot be paginated,
and gqlfreez reports an error instead of writing only the first page.
Paging stops at --max-pages (20, which is 2000 nodes with the default --page-size of
100) and fails instead of writing an incomplete file. Raise both for a large archive.
Careful with silent limits. Some servers cut a connection short without saying so: WPGraphQL returns 100 nodes for
posts(first: 2000), with a normal HTTP 200 and no error.gqlfreezcannot detect this —hasNextPageis the only reliable signal, and it is only in the response if the query asked for it. Leavefirstout and you get everything.
gqlfreez reads your existing graphql-config.
Declarative formats only (.graphqlrc, .graphqlrc.{yml,yaml,json,toml},
graphql.config.*) — a binary cannot run a .js / .ts config file.
schema:
- https://example.com/graphql:
headers:
Authorization: "Bearer ${API_TOKEN}"
extensions:
endpoints:
default:
url: https://example.com/graphql
headers:
Authorization: "Bearer ${API_TOKEN}"Headers under schema are what give you autocompletion and schema checking inside
.graphql files in VS Code and JetBrains. ${VAR} and ${VAR:default} are read from the
environment, and from .env / .env.local unless you pass --no-dotenv.
- name: Install gqlfreez
run: |
curl -fsSL https://github.com/lexoyo/gqlfreez/releases/latest/download/gqlfreez-linux-x64 \
-o /usr/local/bin/gqlfreez
chmod +x /usr/local/bin/gqlfreez
- name: Freeze the data
run: gqlfreez ./src
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
- run: npm run build # or hugo, or zola build, or whatever you useUse a fixed version instead of latest if you want repeatable builds: replace
releases/latest/download with releases/download/v0.1.0.
Useful flags: --check fails when a frozen file is out of date, and writes nothing;
--dry-run writes nothing at all; --concurrency defaults to 1 because shared WordPress
hosting cannot take more; --delay waits between two paginated requests.
Exit codes: 0 all good, 1 a query failed, 2 a configuration problem, 3 --check
found a file out of date.
Version 1 only freezes queries that take no parameters. If you need one query per item
(post(slug: $slug)), variables are next on the roadmap.
- Publish to npm and crates.io, so
npx gqlfreezandcargo install gqlfreezwork (the npm wrapper is written, inwrappers/node, but nothing is published yet) - Query variables
- Several named endpoints (
# @endpoint:per file) - Collect every error instead of stopping at the first one
- Full retry policy (exponential backoff, jitter)
- Shared fragments with
#import - Backward and nested pagination
- Merge identical queries across files
- Service mode (RPC over stdin/stdout) for generator plugins with hot reload
The shape of the tool — a single binary that reads and writes files instead of calling a generator's API — comes from Pagebreak and Pagefind. That is what lets one binary serve every ecosystem.
The pagination convention comes from
@octokit/plugin-paginate-graphql
and from graphql-fetch-optimizer,
an earlier tool for the same problem.
GPL-3.0