Log in Start free trial →
Reference
Fontdue.jsGraphQLDemo templateGuides
WebflowWordpress + Lay ThemeWordpress + SempliceNext.js (App Router)Next.js (Pages Router)Python + FlaskInformation
What’s newFull-service website developmentManaging your font catalogVariable fontsFont licensesWebfontsCross-origin accessPayments with StripeTaxTest fontsTest modeManage your Fontdue subscriptionLaunching your siteTroubleshootingUpdates
Update 001Update 002Update 003© Fontdue, 2022
Next.js (App Router)
In this guide we’ll create an example Fontdue integration for Next.js App Router, using the Fontdue.js React components and querying the GraphQL API for data.
If you prefer to use the Next.js pages router, see our old documentation here.
Start from our example repo
Start from scratch
We’ll get started with a default Next.js installation using TypeScript:
npx create-next-app@latest
We used the following settings:
✔ What is your project named? … my-app
✔ Would you like to use TypeScript with this project? … Yes
✔ Would you like to use ESLint with this project? … Yes
✔ Would you like to use Tailwind CSS with this project? … No
✔ Would you like to use `src/` directory with this project? … Yes
✔ Use App Router (recommended)? … Yes
✔ Would you like to customize the default import alias? … No
Allow API access
When you start the server with npm run dev
, your site will be served on http://localhost:3000
. Add this URL to your Fontdue Cross-origin API access setting.
Add Fontdue.js
For details about the React components, read the docs on NPM →
npm install fontdue-js@latest
Create a file .env.local
:
NEXT_PUBLIC_FONTDUE_URL=https://your-site.fontdue.com
Replace your-site.fontdue.com
with your Fontdue URL.
Modify src/app/layout.tsx
to add the FontdueProvider
, StoreModal
and the fontdue.css
:
// src/app/layout.tsx
import FontdueProvider from "fontdue-js/FontdueProvider";
import StoreModal from "fontdue-js/StoreModal";
import "fontdue-js/fontdue.css";
import "./globals.css";
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<FontdueProvider>
{children}
<StoreModal />
</FontdueProvider>
</body>
</html>
);
}
Add Fontdue components
Modify src/app/pages.tsx
and try adding a Fontdue component, for example:
// src/app/pages.tsx
import TypeTesters from "fontdue-js/TypeTesters";
import styles from "./page.module.css";
export default function Home() {
return (
<main className={styles.main}>
<TypeTesters collectionSlug="ibm-plex" autofit />
</main>
);
}
(Replace ibm-plex
with a font collection slug from your site)
It’s possible to stop here and use your own solution for managing data on your site. The next section shows how you can consume all the content from the Fontdue CMS to power your site.
Query the GraphQL API
The following provides an example setup for querying the Fontdue GraphQL API for static site generation using TypeScript.
Query functions
Add this example function for fetching Fontdue data.
// src/lib/graphql.ts
import { promises as fs } from "fs";
import crypto from "crypto";
import path from "path";
const ENDPOINT = `${process.env.NEXT_PUBLIC_FONTDUE_URL}/graphql`;
const getStaticQuery = async (queryName: string) => {
let query = await fs.readFile(
path.resolve(process.cwd(), "src", "queries", queryName),
"utf8"
);
return query;
};
const fetchGraphql = async <Q, V = void>(
queryName: string,
variables: V | void
): Promise<Q> => {
const hash = crypto
.createHash("md5")
.update(`${JSON.stringify(variables)}`)
.digest("hex")
.slice(0, 6);
const query = await getStaticQuery(queryName);
const response = await fetch(`${ENDPOINT}?query=${queryName}&hash=${hash}`, {
method: "POST",
body: JSON.stringify({ query, variables }),
headers: {
"content-type": "application/json",
},
next: {
tags: ["graphql"],
},
});
if (response.status !== 200) {
throw new Error("Fontdue request failed");
}
const json = await response.json();
const errorMessage = json.errors?.[0]?.message;
if (errorMessage) {
throw new Error(`Fontdue graphql request error: ${errorMessage}`);
}
return json.data;
};
export { fetchGraphql, getStaticQuery };
We can use these functions to query some data for the index.
Modify src/app/page.tsx
importing the new functions and fetching data at the top of the component. This takes advantage of Next’s new server component fetching.
// src/app/page.tsx
import TypeTesters from "fontdue-js/TypeTesters";
import styles from "./page.module.css";
import { fetchGraphql } from "@/lib/graphql";
export default async function Home() {
const data = await fetchGraphql("Index.graphql");
return (
<main className={styles.main}>
<TypeTesters collectionSlug="00-hypercube" autofit />
</main>
);
}
Then add Index.graphql
. For example:
# src/queries/Index.graphql
query Index {
viewer {
fontCollections(onlyRoots: true, first: 10) {
edges {
node {
id
name
slug {
name
}
}
}
}
}
}
You should see response data for the query logged now.
Note though that the data has the type unknown
. We can get TypeScript types for our GraphQL queries using GraphQL code generator.
GraphQL Code Generator
It’s possible to generate the TypeScript types for all of the queries we specify in the src/queries
directory, so that we get proper types for use in our components. Let’s set that up:
npm install --save-dev graphql @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations dotenv
Add a codegen.yml
file with this configuration.
schema: ${NEXT_PUBLIC_FONTDUE_URL}/graphql
documents: './src/queries/*.graphql'
generates:
operations-types.ts:
config:
onlyOperationTypes: true
preResolveTypes: true
skipTypename: true
avoidOptionals: true
plugins:
- typescript
- typescript-operations
Update package.json
to add codegen
to the "scripts"
section. We use dotenv
to insert our Fontdue URL environment variable.
"codegen": "DOTENV_CONFIG_PATH=.env.local graphql-codegen -r dotenv/config --watch"
You may want to incorporate the codegen into the dev
script, for example:
npm install --save-dev npm-run-all
"dev": "run-p next-dev codegen",
"next-dev": "next dev",
"codegen": "DOTENV_CONFIG_PATH=.env.local graphql-codegen -r dotenv/config --watch"
With the codegen
script running, we should see a new file in the root directory: operations-types.ts
. This will include an IndexQuery
type.
Note, the name of the type comes from the name of the query inside Index.graphql
(not the name of the file).
Let’s import the type in our src/app/page.tsx
component:
import { IndexQuery } from '../../operations-types';
Optionally, provide a path alias for this in tsconfig.json
:
{
"compilerOptions": {
...
"paths": {
"@/*": ["./src/*"],
"@graphql": ["./operations-types.ts"]
}
},
...
}
import { IndexQuery } from '@graphql';
At this stage you have the tools to build out all your pages consuming data from your Fontdue API. The examples below provide some more guidance on how you might query data for common use-cases.
Create links to fonts pages
Our example Index
query fetches font collections along with their slugs, so we can create a list of links to their respective font pages:
// src/app/page.tsx
import Link from "next/link";
import styles from "./page.module.css";
import { fetchGraphql } from "@/lib/graphql";
import { IndexQuery } from "@graphql";
export default async function Home() {
const data = await fetchGraphql<IndexQuery>("Index.graphql");
return (
<main className={styles.main}>
<ul>
{data.viewer?.fontCollections?.edges?.map((edge) => (
<li key={edge!.node!.id}>
<Link href={`/fonts/${edge!.node!.slug?.name}`}>
{edge!.node!.name}
</Link>
</li>
))}
</ul>
</main>
);
}
URL params in queries
Let’s now add a detail page for fonts, which we link to from the homepage.
We’ll add a few files.
# src/queries/Font.graphql
query Font ($slug: String!) {
viewer {
slug(name: $slug) {
fontCollection {
id
name
}
}
}
}
# src/queries/FontPaths.graphql
query FontPaths {
viewer {
fontCollections(first: 999, onlyRoots: true) {
edges {
node {
slug { name }
}
}
}
}
}
// src/lib/utils.ts
export function notEmpty<TValue>(
value: TValue | null | undefined
): value is TValue {
return value !== null && value !== undefined;
}
// src/app/fonts/[slug]/page.tsx
import React from "react";
import TypeTesters from "fontdue-js/TypeTesters";
import BuyButton from "fontdue-js/BuyButton";
import { FontPathsQuery, FontQuery, FontQueryVariables } from "@graphql";
import { fetchGraphql } from "@/lib/graphql";
import { notEmpty } from "@/lib/utils";
interface FontProps {
params: { slug: string };
}
export default async function Font({ params }: FontProps) {
const data = await fetchGraphql<FontQuery, FontQueryVariables>(
"Font.graphql",
{
slug: params.slug as string,
}
);
const font = data.viewer.slug!.fontCollection!;
return (
<div>
<h1>{font.name}</h1>
<BuyButton collectionId={font.id} collectionName={font.name} />
<TypeTesters collectionId={font.id} autofit />
</div>
);
}
export async function generateStaticParams() {
const data = await fetchGraphql<FontPathsQuery>("FontPaths.graphql");
const slugs = data
.viewer!.fontCollections!.edges!.map((edge) => edge?.node?.slug?.name)
.filter(notEmpty);
return slugs.map((slug) => ({ slug }));
}
You should now be able to navigate on the site using links to the font detail pages.
This example demonstrates how you can pass the slug
param from the dynamic path src/app/fonts/[slug]/page.tsx
through to the Font
query’s $slug
variable.
This also demonstrates how to provide Next with a list of font slugs, in order to statically generate all font pages. To do that, we’re querying the GraphQL API for all of our relevant font collections. Notice how we’re querying for onlyRoots: true
, which matches with how we’re querying for collections in the Index
query. You may want detail pages for all collections including subfamilies, in which case you should instead query like so:
fontCollections(collectionTypes: [SUPERFAMILY, FAMILY], first: 999)
Loading webfonts
The latest Next.js conventions for loading fonts (using the next/font
packages) do not allow us to conveniently use the webfonts automatically generated by Fontdue. But we created a workaround: a component that you can use to load the webfonts for any FontStyle
from the GraphQL API. After we query for the webfontSources
, we can use ReactDOM.preload()
which will create relevant <link rel="preload">
elements in the page’s <head>
, ensuring webfonts are loaded early. We then add a <style>
element inline in the page to define the @font-face
declarations, the same way the Fontdue CSS files do, but without requiring an extra request.
// src/components/PreloadWebfonts.tsx
"use client";
import ReactDOM from "react-dom";
interface FontStyleCSS {
cssFamily: string | null;
name: string | null;
webfontSources:
| ({
url: string | null;
format: string | null;
} | null)[]
| null;
}
const createFontFaceStyle = ({
cssFamily,
name,
webfontSources,
}: FontStyleCSS): string => {
const source = webfontSources?.find((source) => source?.format === "woff2");
if (!source) return "";
return `
@font-face {
font-family: "${cssFamily} ${name}";
src: url(${source.url}) format(${source.format});
font-weight: 400;
font-style: normal;
}
`;
};
export default function PreloadWebfonts({
style,
}: {
style: FontStyleCSS | null;
}) {
if (!style) return null;
const source = style.webfontSources?.find(
(source) => source?.format === "woff2"
);
if (source?.url) {
ReactDOM.preload(source.url, { as: "font" });
}
return (
<style
type="text/css"
dangerouslySetInnerHTML={{
__html: createFontFaceStyle(style),
}}
/>
);
}
Usage
We’ll create a realistic Homepage where we list all the font collections, rendered in their own “Feature Style” (selected in the Fontdue admin).
First, in our Index.graphql
file, we query for all root font collections and their associated featureStyle
, including the webfontSources
which we’ll pass to the PreloadWebfonts
component.
# src/queries/Index.graphql
query Index {
viewer {
fontCollections(onlyRoots: true, first: 99) {
edges {
node {
name
featureStyle {
cssFamily
name
webfontSources {
format
url
}
}
}
}
}
}
}
We add a wrapper component to render the font style, using Fontdue useFontStyle
hook, which renders the loading state as a series of dots.
// src/components/FontStyle.tsx
"use client";
import React from "react";
import useFontStyle from "fontdue-js/useFontStyle";
interface FontStyle_props {
familyName: string | null | undefined;
styleName: string | null | undefined;
style?: React.CSSProperties;
children: React.ReactNode;
}
export default function FontStyle({
familyName,
styleName,
style: styleProp,
children,
}: FontStyle_props) {
const { style } = useFontStyle({
fontFamily: `${familyName} ${styleName}`,
fontWeight: "400",
fontStyle: "normal",
});
return <span style={{ ...style, ...styleProp }}>{children}</span>;
}
Now, in our home page we can combine these components to render each font in its own featureStyle
// src/app/page.tsx
import Link from "next/link";
import { fetchGraphql } from "@/lib/graphql";
import { IndexQuery } from "@graphql";
import FontStyle from "@/components/FontStyle";
import PreloadWebfonts from "@/components/PreloadWebfonts";
export default async function Home() {
const data = await fetchGraphql<IndexQuery>("Index.graphql");
return (
<main className="main">
<section className="home">
{data.viewer.fontCollections?.edges?.map((edge) => {
const node = edge!.node!;
if (!node.slug) return;
return (
<h2 key={node.id}>
<PreloadWebfonts style={node.featureStyle} />
<Link href={`/fonts/${node.slug.name}`}>
<FontStyle
familyName={node.featureStyle?.cssFamily}
styleName={node.featureStyle?.name}
>
{node.name}
</FontStyle>
</Link>
</h2>
);
})}
</section>
</main>
);
}
Revalidating data
All of the fetch
queries we make are automatically cached by Next.js. In development, it’s possible to refresh the cache just by telling the browser to not cache (e.g. ⌘⇧R) but in production we need to empty the cache whenever content changes. We can create an endpoint in our Next.js app that Fontdue can call whenever you make any changes in your Fontdue admin. Note that the endpoint must accept POST requests.
// src/app/api/revalidate/route.ts
import { NextRequest, NextResponse } from "next/server";
import { revalidateTag } from "next/cache";
export async function POST(_request: NextRequest) {
revalidateTag("graphql");
return NextResponse.json({ revalidated: true, now: Date.now() });
}
Then, in your Fontdue admin, navigate to Settings → Website settings, find the field Deploy hook URL and enter the endpoint URL like below. Replace the domain name with your site’s production domain.
https://your-site.vercel.app/api/revalidate