📓

Next.js (App Router)

Log in Start free trial →

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.

🚧
This guide is intended for web developers. If you're a type designer interested in building your own site from scratch, we recommend either finding a web developer to work with or inquiring with us about our web development service. Learn more

If you prefer to use the Next.js pages router, see our old documentation here.

Start from our example repo

📖
You can skip much of this guide and clone our example repo. All of the functions explained in this guide are captured and working in the example (including many more pages), so you can start from there and make your own customizations. Clone the demo 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 path from 'path';

const ENDPOINT = `${process.env.NEXT_PUBLIC_FONTDUE_URL}/graphql`;


const getStaticQuery = async (queryName: string) => {
  return await fs.readFile(
    path.resolve(process.cwd(), 'src', 'queries', queryName),
    'utf8'
  );
};
const fetchGraphql = async <Q, V = void>(
  queryName: string,
  variables: V | void
): Promise<Q> => {
  const query = await getStaticQuery(queryName);
  const response = await fetch(ENDPOINT, {
    method: 'POST',
    body: JSON.stringify({ query, variables }),
    headers: {
      'content-type': 'application/json',
    },
  });

  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 { revalidatePath } from 'next/cache'

export async function POST(request: NextRequest) {
  const path = request.nextUrl.searchParams.get('path') || '/'
  revalidatePath(path)
  return NextResponse.json({ revalidated: true, now: Date.now() })
}

Then, in your Fontdue admin, navigate to SettingsWebsite 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