FontdueProvider is the one piece of Fontdue you mount yourself, once, at the top of your app. It opens the connection to your store, holds the shared cart state every component reads from, and renders the ambient parts of Fontdue that aren’t tied to a single component – your theme, the test-mode and consent banners, and the admin preview toolbar. Everything else in fontdue-js assumes it sits above them.
It goes in your root layout, alongside the Store Modal – the cart and checkout overlay, which renders nothing until a customer opens the cart, so it belongs in the layout once rather than on every page.
Mounting the provider
You mount the provider and the Store Modal once, at the top of your app. The one thing to get right is how you supply the provider’s data.
In Next.js, wrap your layout’s children in the provider and put the Store Modal inside it. The provider reads your store URL from the environment, so the only prop it needs is a config object for theme and component defaults. Each component below it renders on the server through React Server Components, so there’s no preloading to wire up here.
Load the provider’s data in your root route’s loader with loadFontdueProviderQuery() and pass it as preloadedQuery, then wrap your app in the provider with the Store Modal inside. Preloading on the server keeps the page from flashing unstyled while the theme loads.11This is the preloaded pattern applied to the provider itself – the same one you’ll use for components on these frameworks.
Astro renders each React component as its own island – independent roots in the page, not one shared tree – so the provider can’t wrap your content the way it does on other frameworks. Mount it as a standalone island alongside your page, preload its data with loadFontdueProviderQuery() in your layout’s frontmatter, and render the Store Modal as its own island too. The components still share your store connection and cart state through fontdue-js’s client-side state, so they don’t need to sit inside the provider’s tree.22Because there’s no shared provider tree to inherit from, each Astro component island also takes the same config prop directly – keep it in one module and import it where you render an island.
// src/app/layout.tsx
import FontdueProvider from "fontdue-js/FontdueProvider";
import StoreModal from "fontdue-js/StoreModal";
import "fontdue-js/fontdue.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<FontdueProvider config={{ typeTester: { selectable: true } }}>
{children}
<StoreModal />
</FontdueProvider>
</body>
</html>
);
}
// app/root.tsx — trimmed; the template's root also renders the html/head scaffolding
import FontdueProvider, { loadFontdueProviderQuery } from "fontdue-js/FontdueProvider";
import StoreModal from "fontdue-js/StoreModal";
import "fontdue-js/fontdue.css";
export async function loader() {
return { fontduePreload: await loadFontdueProviderQuery() };
}
export default function App({ loaderData }: Route.ComponentProps) {
return (
<FontdueProvider
preloadedQuery={loaderData.fontduePreload}
config={{ typeTester: { selectable: true } }}
>
<Outlet />
<StoreModal />
</FontdueProvider>
);
}
// src/routes/__root.tsx — trimmed; the template's root also renders the html/head scaffolding
import FontdueProvider, { loadFontdueProviderQuery } from "fontdue-js/FontdueProvider";
import StoreModal from "fontdue-js/StoreModal";
export const Route = createRootRoute({
loader: async () => ({ fontduePreload: await loadFontdueProviderQuery() }),
component: RootComponent,
});
function RootComponent() {
const { fontduePreload } = Route.useLoaderData();
return (
<FontdueProvider
preloadedQuery={fontduePreload}
config={{ typeTester: { selectable: true } }}
>
<Outlet />
<StoreModal />
</FontdueProvider>
);
}
---
// src/layouts/Layout.astro
import FontdueProvider, { loadFontdueProviderQuery } from "fontdue-js/FontdueProvider";
import StoreModal from "fontdue-js/StoreModal";
import "fontdue-js/fontdue.css";
const fontduePreload = await loadFontdueProviderQuery();
---
<html lang="en">
<body>
<FontdueProvider client:load preloadedQuery={fontduePreload} />
<slot />
<StoreModal client:load />
</body>
</html>
The config object
A config object sets component defaults – selectable type-tester text, where each tester puts its variable-axis controls, and so on – so behavior lives in one place instead of being repeated prop by prop. The full set of options is in the configuration reference.
You pass it once, to the provider. The config prop sets the defaults for every component below it in the tree, and per-component props override them wherever you need:
config={{ typeTester: { selectable: true, variableAxesPosition: "auto" } }}
On the other frameworks the provider hands its config down through React context, so every component below it inherits the defaults. Astro’s island architecture breaks that inheritance: each component is its own React root, not a descendant of the provider, so a config set on the provider never reaches the type testers or any other component33The same island boundary that keeps the provider from wrapping your content – see Mounting the provider above.. Keep the object in one module instead, typed against Config so a bad key is caught at compile time:
// src/lib/fontdue.ts
import type { Config } from "fontdue-js";
export const fontdueConfig = {
typeTester: { selectable: true },
} satisfies Config;
Then import that one object wherever you mount a component, and pass it to that component’s own config prop:
---
// src/pages/index.astro
import TypeTester from "fontdue-js/TypeTester";
import { fontdueConfig } from "../lib/fontdue";
---
<TypeTester client:load preloadedQuery={preload} config={fontdueConfig} />
The module is the single source of truth; per-component props override its defaults on the one component they’re set on.
With the provider mounted, every Fontdue component can reach your store – so the next page puts one on a page.
config prop directly – keep it in one module and import it where you render an island. ↩