Static Export & Sources
Pre-render every page to a plain HTML file and deploy to any static host — no server required. Pull data from any CMS or API using Gatsby-compatible source plugins.
hadars export static
hadars export static builds the project and pre-renders a list of URL paths to index.html files. Each page also gets an index.json sidecar so useServerData keeps working on client-side navigation without a live server.
# Output goes to out/ by default
hadars export static
# Custom output directory
hadars export static distMinimal config
Add a paths function to hadars.config.ts that returns the list of URLs to pre-render. That's all that's required.
// hadars.config.ts
import type { HadarsOptions } from 'hadars';
export default {
entry: './src/App.tsx',
paths: () => ['/', '/about', '/contact'],
} satisfies HadarsOptions;Output layout
out/
├── index.html # /
├── index.json # useServerData sidecar for /
├── about/
│ ├── index.html # /about
│ └── index.json
├── contact/
│ ├── index.html # /contact
│ └── index.json
└── static/ # JS, CSS, fonts — copied from .hadars/static/
├── index.js
└── ...Serve out/ from any static host — Vercel, Netlify, Cloudflare Pages, S3, or a plain nginx. No server-side code required.
Data in static pages
getInitProps receives a HadarsStaticContext as its second argument during static export. Use it to fetch data from a database, API, or the GraphQL layer (see below).
// src/App.tsx
import type { HadarsApp, HadarsRequest, HadarsStaticContext } from 'hadars';
interface Props { posts: Post[] }
const App: HadarsApp<Props> = ({ posts }) => (
<ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
);
export const getInitProps = async (
req: HadarsRequest,
ctx?: HadarsStaticContext,
): Promise<Props> => {
// ctx is only present during static export (and dev with sources configured)
if (!ctx) return { posts: [] };
const { data } = await ctx.graphql('{ allPost { id title } }');
return { posts: data?.allPost ?? [] };
};
export default App;Source plugins
hadars source plugins follow the same API as Gatsby's sourceNodes — so most existing Gatsby CMS source plugins work out of the box. Each plugin creates typed nodes in an in-memory store; hadars infers a GraphQL schema automatically and exposes it to paths() and getInitProps().
During hadars dev, a GraphiQL IDE is served at /__hadars/graphql so you can explore the inferred schema while you build.
Install graphql
Schema inference requires graphql to be installed in your project:
npm install graphqlConfig
Add a sources array to your config. Each entry mirrors Gatsby's plugin format: a resolve (package name or pre-imported module) and an optional options object.
// hadars.config.ts
import type { HadarsOptions, HadarsStaticContext } from 'hadars';
export default {
entry: './src/App.tsx',
sources: [
{
resolve: 'gatsby-source-contentful',
options: {
spaceId: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
},
},
],
paths: async ({ graphql }: HadarsStaticContext) => {
const { data } = await graphql(`{
allContentfulBlogPost { slug }
}`);
const slugs = data?.allContentfulBlogPost?.map((p: any) => p.slug) ?? [];
return ['/', ...slugs.map((s: string) => `/post/${s}`)];
},
} satisfies HadarsOptions;Local source plugin
Pass a pre-imported module instead of a package name to use a local plugin without publishing it to npm. The module must export a sourceNodes function.
// src/posts-source.ts
export async function sourceNodes(
{ actions, createNodeId, createContentDigest, reporter }: any,
options: { dataDir: string } = {},
) {
const { createNode } = actions;
const posts = await fetchPostsFromMyApi();
for (const post of posts) {
createNode({
...post,
id: createNodeId(post.slug),
internal: {
type: 'BlogPost',
contentDigest: createContentDigest(post),
},
});
}
reporter.info(`Created ${posts.length} BlogPost nodes`);
}// hadars.config.ts
import * as postsSource from './src/posts-source';
export default {
entry: './src/App.tsx',
sources: [
{ resolve: postsSource },
],
paths: async ({ graphql }) => {
const { data } = await graphql('{ allBlogPost { slug } }');
return ['/', ...(data?.allBlogPost ?? []).map((p: any) => `/post/${p.slug}`)];
},
} satisfies HadarsOptions;Inferred GraphQL schema
hadars inspects the fields on each node type and generates a GraphQL schema automatically. For each type (e.g. BlogPost) you get two root queries:
allBlogPostEvery BlogPost nodeblogPost(id, slug, title, …)First node matching all supplied argsScalar fields are automatically added as lookup arguments on the single-item query — so you can do blogPost(slug: "hello") without knowing the hashed node id.
# Explore your schema at /__hadars/graphql in dev mode
{
allBlogPost {
id
slug
title
date
}
blogPost(slug: "hello-world") {
title
body
}
}useGraphQL hook
Query your GraphQL layer directly inside any component — no need to thread data through getInitProps. The hook integrates with useServerData so queries run on the server during static export and hydrate on the client at no extra cost.
import { useGraphQL } from 'hadars';
import { GetAllPostsDocument } from './gql/graphql';
const PostList = () => {
const result = useGraphQL(GetAllPostsDocument);
const posts = result?.data?.allBlogPost ?? [];
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
};Pass variables as a second argument. When a typed DocumentNode from graphql-codegen is used, result.data has the exact inferred shape of your query — no casting needed.
const PostPage = ({ slug }: { slug: string }) => {
const result = useGraphQL(GetPostDocument, { slug });
const post = result?.data?.blogPost;
if (!post) return null;
return <h1>{post.title}</h1>;
};result is undefined on the first SSR pass while the query is pending — render null or a skeleton. GraphQL errors throw during static export so the page is marked as failed rather than silently serving incomplete data.
GraphQL fragments
graphql-codegen's client preset generates fragment masking helpers (FragmentType, useFragment) that let components co-locate their exact data requirements. No hadars changes are needed — just define your fragment with the graphql() tag and accept a masked prop:
// src/PostCard.tsx
import { graphql, useFragment, type FragmentType } from './gql';
export const PostCardFragment = graphql(`
fragment PostCard on BlogPost {
slug
title
date
}
`);
interface Props { post: FragmentType<typeof PostCardFragment> }
const PostCard = ({ post: postRef }: Props) => {
const post = useFragment(PostCardFragment, postRef);
return (
<article>
<h2>{post.title}</h2>
<time>{post.date}</time>
</article>
);
};The parent component spreads the raw node into the masked prop — TypeScript ensures it satisfies the fragment shape without any manual type assertions:
const PostList = () => {
const result = useGraphQL(GetAllPostsDocument);
return (
<>
{result?.data?.allBlogPost.map(post => (
<PostCard key={post.slug} post={post} />
))}
</>
);
};Schema export & type generation
Run hadars export schema to write the inferred schema to a SDL file, then use graphql-codegen to generate TypeScript types for your queries. Also works with a custom graphql executor — hadars introspects it automatically.
# 1. Generate schema.graphql from your sources
hadars export schema
# Custom output path
hadars export schema types/schema.graphql
# 2. Install codegen (one-time)
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
# 3. Generate types
npx graphql-codegen --schema schema.graphql --documents "src/**/*.tsx" --out src/gql/Or add a codegen.ts config file for more control:
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'schema.graphql',
documents: ['src/**/*.tsx'],
generates: {
'src/gql/': {
preset: 'client',
},
},
};
export default config;Supported Gatsby context API
The following Gatsby sourceNodes context properties are implemented:
actions.createNodeAdds a node to the storeactions.deleteNodeNo-op (not needed on initial build)actions.touchNodeNo-opcreateNodeId(input)SHA-256 of pluginName + inputcreateContentDigest(obj)MD5 of JSON.stringify(obj)getNode(id)Look up a node by idgetNodes()All nodes in the storegetNodesByType(type)All nodes of a given typecache.get / cache.setIn-memory per-plugin cachereporter.info/warn/error/panicLogs to consoleemitterReal EventEmitter — BOOTSTRAP_FINISHED is emitted after settlingCustom GraphQL executor
If you prefer to manage your own schema — or need full control over resolvers — skip sources and provide a graphql executor directly. It will be passed to both paths() and getInitProps() as ctx.graphql.
import { graphql, buildSchema } from 'graphql';
import type { HadarsOptions } from 'hadars';
const schema = buildSchema(`
type Post { id: ID! title: String slug: String }
type Query { allPost: [Post!]! }
`);
const rootValue = {
allPost: () => fetchPostsFromDb(),
};
export default {
entry: './src/App.tsx',
graphql: (query, variables) =>
graphql({ schema, rootValue, source: query, variableValues: variables }),
paths: async ({ graphql }) => {
const { data } = await graphql('{ allPost { slug } }');
return ['/', ...(data?.allPost ?? []).map((p: any) => `/post/${p.slug}`)];
},
} satisfies HadarsOptions;