-* Simple way to add webfonts or custom fonts to Gatsby project
-* Performant asynchronous font loading can be enabled
-* Font loading listener can be enabled
-* Flash Of Unstyled Text (FOUT) handling support
+- Simple way to add webfonts or custom fonts to Gatsby project
+- Performant asynchronous font loading can be enabled
+- Font loading listener can be enabled
+- Flash Of Unstyled Text (FOUT) handling support
@@ -15,17 +15,18 @@
+
## Features
-* Supports web fonts & self-hosted fonts
-* Preloads the files & preconnects to the URL
-* Loads fonts asynchronously to avoid render blocking
-* Implemented with [fast loading snippets](https://csswizardry.com/2020/05/the-fastest-google-fonts/)
-* Loading status listener for avoiding FOUT
-* Small size & minimal footprint
+- Supports web fonts & self-hosted fonts
+- Preloads the files & preconnects to the URL
+- Loads fonts asynchronously to avoid render blocking
+- Implemented with [fast loading snippets](https://csswizardry.com/2020/05/the-fastest-google-fonts/)
+- Loading status listener for avoiding FOUT
+- Small size & minimal footprint
## Install
@@ -105,12 +106,12 @@ Add the following snippet to `gatsby-config.js` plugins array.
false
-
interval
+
interval (V1 ONLY)
Works if enableListener is true. Font listener interval (in ms). Default is 300ms. Recommended: >=300ms.
300
-
timeout
+
timeout (V1 ONLY)
Works if enableListener is true. Font listener timeout value (in ms). Default is 30s (30000ms). Listener will no longer check for loaded fonts after timeout, fonts will still be loaded and displayed, but without handling FOUT.
30000
@@ -138,21 +139,22 @@ Add the following snippet to `gatsby-config.js` plugins array.
## Async mode vs Render-blocking mode
+
### Async mode
+
Load font stylesheets and files in low-priority mode. If you want to add fonts in a performant way, handle FOUT on your own and make sure that the page render times are low, you should use `async` mode.
-__Pros:__ Performance, content is displayed before font files are downloaded and parsed
+**Pros:** Performance, content is displayed before font files are downloaded and parsed
-__Cons:__ FOUT needs to be handled
+**Cons:** FOUT needs to be handled
### Render-blocking mode
+
Load font stylesheets and files in high-priority mode. If you want to use this plugin as a simple way to add fonts to your project as you would do in any other project, without any performance optimizations and FOUT handling, you should use `render-blocking` mode.
-__Pros:__ Simple markup, FOUT won't occur in most cases
+**Pros:** Simple markup, FOUT won't occur in most cases
-__Cons:__ Font stylesheets and font files can delay first content paint time
-
-
+**Cons:** Font stylesheets and font files can delay first content paint time
## Handling FOUT with Font loading listener
@@ -162,18 +164,24 @@ To avoid this, we can use CSS to style the fallback font to closely match the fo
When `enableListener: true` is set in plugin config in `gatsby-config.js`, HTML classes are being added to `` element as the fonts are being loaded.
-HTML class name format will be in the following format `wf-[font-family-name]--loaded`.
+HTML class name format will be in the following format `wf-[font-family-name]`. When all fonts are loaded `wf-all` is applied.
You can use the [Font Style Matcher](https://meowni.ca/font-style-matcher/) to adjust the perfect fallback font and fallback CSS config.
Here is the example of how `body` element will look like after all fonts are being loaded (depending on the config).
```html
-
+
```
+## V2 breaking changes
+* Removed `interval` and `timeout` options
+* Changed class name format to a more generic `wf-[font-family-name]` to avoid mixing naming conventions
+
## Issues and Contributions
Feel free to [report issues](https://github.com/codeAdrian/gatsby-omni-font-loader/issues) you find and feel free to contribute to the project by creating Pull Requests.
@@ -184,16 +192,10 @@ Contributions are welcome and appreciated!
Thank you for your contribution!
-[Henrik](https://github.com/henrikdahl) • [Lennart](https://github.com/LekoArts) • [Francis Champagne](https://github.com/fcisio)
+[Henrik](https://github.com/henrikdahl) • [Lennart](https://github.com/LekoArts) • [Francis Champagne](https://github.com/fcisio) • [Hugo](https://github.com/hugofabricio)
-## Sponsors
+## Supported by
Thank you for your support!
-[Roboto Studio](https://roboto.studio/) • [Your Name Here](https://www.buymeacoffee.com/ubnZ8GgDJ/e/11337)
-
-## Support
-
-The project is created and maintained by [Adrian Bece](https://codeadrian.github.io/) with the generous help of community contributors. If you have used the plugin and would like to contribute, feel free to [Buy Me A Coffee](https://www.buymeacoffee.com/ubnZ8GgDJ).
-
-
+[Roboto Studio](https://roboto.studio/)
diff --git a/components/FontListener.tsx b/components/FontListener.tsx
deleted file mode 100644
index f2c6b27..0000000
--- a/components/FontListener.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from "react"
-import { hookOptions, useFontListener } from "../hooks"
-
-interface Props {
- options: hookOptions
-}
-
-export const FontListener: React.FC = ({ children, options }) => {
- useFontListener(options)
-
- return <>{children}>
-}
diff --git a/components/index.ts b/components/index.ts
index 3ab98aa..9594226 100644
--- a/components/index.ts
+++ b/components/index.ts
@@ -1,2 +1 @@
-export * from "./AsyncFonts"
-export * from "./FontListener"
+export * from "./AsyncFonts";
diff --git a/consts/defaults.ts b/consts/defaults.ts
index 30296b5..da455bd 100644
--- a/consts/defaults.ts
+++ b/consts/defaults.ts
@@ -1,7 +1,2 @@
-export const INTERVAL_DEFAULT = 300
-
-export const TIMEOUT_DEFAULT = 30000
-
-export const MODE_DEFAULT = "async"
-
-export const SCOPE_DEFAULT = "body"
+export const MODE_DEFAULT = "async";
+export const SCOPE_DEFAULT = "body";
diff --git a/gatsby-browser.js b/gatsby-browser.js
index dff81f0..4ba46ad 100644
--- a/gatsby-browser.js
+++ b/gatsby-browser.js
@@ -1,48 +1,41 @@
-import React from "react"
-import { AsyncFonts, FontListener } from "./components"
-import {
- INTERVAL_DEFAULT,
- MODE_DEFAULT,
- TIMEOUT_DEFAULT,
- SCOPE_DEFAULT,
-} from "./consts"
-import { getFontFiles, getFontNames } from "./utils"
+import React from "react";
+import { AsyncFonts } from "./components";
+import { MODE_DEFAULT, SCOPE_DEFAULT } from "./consts";
+import { getFontFiles, getFontNames } from "./utils";
+import { fontListener } from "./utils/fontListener";
+
+export const onClientEntry = (
+ _,
+ { custom = [], web = [], enableListener = false, scope = SCOPE_DEFAULT }
+) => {
+ if (!enableListener) {
+ return;
+ }
+
+ const allFonts = [...custom, ...web];
+ const fontNames = getFontNames(allFonts);
+ const listenerProps = { fontNames, scope };
+
+ fontListener(listenerProps);
+};
export const wrapRootElement = (
{ element },
- {
- custom = [],
- web = [],
- enableListener,
- interval = INTERVAL_DEFAULT,
- timeout = TIMEOUT_DEFAULT,
- scope = SCOPE_DEFAULT,
- mode = MODE_DEFAULT,
- }
+ { custom = [], web = [], mode = MODE_DEFAULT }
) => {
if (mode !== "async") {
- return element
+ return element;
}
- const allFonts = [...custom, ...web]
- const fontFiles = getFontFiles(allFonts)
- const fontNames = getFontNames(allFonts)
+ const allFonts = [...custom, ...web];
+ const fontFiles = getFontFiles(allFonts);
+ const fontNames = getFontNames(allFonts);
+ const hasFontNames = Boolean(fontNames.length);
- const listenerProps = { fontNames, interval, timeout, scope }
-
- const hasFontFiles = Boolean(fontFiles.length)
- const hasFontNames = Boolean(fontNames.length)
-
- const children = (
+ return (
<>
{hasFontNames && }
{element}
>
- )
-
- if (!hasFontFiles || !enableListener) {
- return children
- }
-
- return {children}
-}
+ );
+};
diff --git a/gatsby-ssr.js b/gatsby-ssr.js
index a4531fa..cef1b2a 100644
--- a/gatsby-ssr.js
+++ b/gatsby-ssr.js
@@ -1,34 +1,21 @@
-import { MODE_DEFAULT } from "./consts"
-import { getFontConfig, getTestFonts } from "./generators"
-import { getFontFiles, getFontNames } from "./utils"
+import { MODE_DEFAULT } from "./consts";
+import { getFontConfig } from "./generators";
+import { getFontFiles } from "./utils";
export const onRenderBody = (
- { setHeadComponents, setPostBodyComponents },
- {
- enableListener,
- preconnect = [],
- preload = [],
- web = [],
- custom = [],
- mode = MODE_DEFAULT,
- }
+ { setHeadComponents },
+ { preconnect = [], preload = [], web = [], custom = [], mode = MODE_DEFAULT }
) => {
- const allFonts = [...web, ...custom]
- const allPreloads = preload.concat(getFontFiles(allFonts))
- const fontNames = getFontNames(allFonts)
+ const allFonts = [...web, ...custom];
+ const allPreloads = preload.concat(getFontFiles(allFonts));
const preloadConfig = getFontConfig(
preconnect,
allPreloads,
mode === "async" ? [] : allFonts
- )
-
- if (enableListener && Boolean(allFonts.length) && mode === "async") {
- const testFontConfig = getTestFonts(fontNames)
- setPostBodyComponents(testFontConfig)
- }
+ );
if (preloadConfig && Boolean(preloadConfig.length)) {
- setHeadComponents(preloadConfig)
+ setHeadComponents(preloadConfig);
}
-}
+};
diff --git a/generators/getFontConfig.tsx b/generators/getFontConfig.tsx
index 78c7917..ae1515b 100644
--- a/generators/getFontConfig.tsx
+++ b/generators/getFontConfig.tsx
@@ -24,7 +24,7 @@ export const getFontConfig = (
if (arrayCheck(preloadConfig)) {
preloadConfig.forEach(href => {
headComponents.push(
-
+
)
})
}
diff --git a/generators/getTestFonts.tsx b/generators/getTestFonts.tsx
deleted file mode 100644
index 7c9eba0..0000000
--- a/generators/getTestFonts.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from "react"
-
-export const getTestFonts = (fontNames: string[]) => {
- const fontConfig = []
-
- const hiddenStyles: React.CSSProperties = {
- position: "absolute",
- overflow: "hidden",
- clip: "rect(0 0 0 0)",
- height: "1px",
- width: "1px",
- margin: "-1px",
- padding: "0",
- border: "0",
- }
-
- fontNames.forEach(fontName => {
- fontConfig.push(
-
-
-
- )
- })
-
- return (
-
- {fontConfig}
-
- )
-}
diff --git a/generators/index.ts b/generators/index.ts
index 2cd9208..e52d557 100644
--- a/generators/index.ts
+++ b/generators/index.ts
@@ -1,2 +1 @@
-export * from "./getFontConfig"
-export * from "./getTestFonts"
+export * from "./getFontConfig";
diff --git a/hooks/index.ts b/hooks/index.ts
deleted file mode 100644
index 0e91a1a..0000000
--- a/hooks/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./useFontListener"
diff --git a/hooks/useFontListener.tsx b/hooks/useFontListener.tsx
deleted file mode 100644
index 5e2d1a4..0000000
--- a/hooks/useFontListener.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import { useEffect, useMemo, useRef, useState } from "react"
-import { kebabCase } from "../utils"
-
-declare var document: { fonts: any }
-
-export type hookOptions = {
- fontNames: string[]
- interval: number
- timeout: number
- scope: string
-}
-
-type fontListenerHook = (options: hookOptions) => void
-
-export const useFontListener: fontListenerHook = ({
- fontNames,
- interval,
- timeout,
- scope,
-}) => {
- const [hasLoaded, setHasLoaded] = useState(false)
- const [loadedFonts, setLoadedFonts] = useState([])
- const [intervalId, setIntervalId] = useState(-1)
- const attempts = useRef(Math.floor(timeout / interval))
-
- const hasFonts = fontNames && Boolean(fontNames.length)
-
- const pendingFonts = useMemo(
- () => fontNames.filter(fontName => !loadedFonts.includes(fontName)),
- [loadedFonts, fontNames]
- )
- const targetElement = useMemo(
- () => (scope === "html" ? "documentElement" : "body"),
- [scope]
- )
-
- const apiAvailable = "fonts" in document
-
- useEffect(() => {
- if (!apiAvailable) {
- handleApiError("Font loading API not available")
- return
- }
-
- if (hasFonts && apiAvailable && !hasLoaded && intervalId < 0) {
- const id = window.setInterval(isFontLoaded, interval)
- setIntervalId(id)
- }
- }, [hasFonts, hasLoaded, intervalId, apiAvailable])
-
- useEffect(() => {
- if (hasLoaded && intervalId > 0) {
- clearInterval(intervalId)
- }
- }, [hasLoaded, intervalId])
-
- function errorFallback() {
- setHasLoaded(true)
- setLoadedFonts(fontNames)
- fontNames.forEach(addClassName)
- }
-
- function handleApiError(error) {
- console.info(`document.fonts API error: ${error}`)
- console.info(`Replacing fonts instantly. FOUT handling failed.`)
- errorFallback()
- }
-
- function addClassName(fontName) {
- document[targetElement].classList.add(`wf-${kebabCase(fontName)}--loaded`)
- }
-
- function isFontLoaded() {
- const loaded = []
- attempts.current = attempts.current - 1
-
- if (attempts.current < 0) {
- handleApiError("Interval timeout reached, maybe due to slow connection.")
- }
-
- const fontsLoading = pendingFonts.map(fontName => {
- let hasLoaded = false
- try {
- hasLoaded = document.fonts.check(`12px '${fontName}'`)
- } catch (error) {
- handleApiError(error)
- return
- }
-
- if (hasLoaded) {
- addClassName(fontName)
- loaded.push(fontName)
- }
-
- return hasLoaded
- })
-
- const allFontsLoaded = fontsLoading.every(font => font)
-
- if (Boolean(loaded.length)) {
- setLoadedFonts(loaded)
- }
-
- if (allFontsLoaded) {
- setHasLoaded(true)
- }
- }
-}
diff --git a/package.json b/package.json
index 52b9446..e2ec4bc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "gatsby-omni-font-loader",
- "version": "1.3.1",
+ "version": "2.0.2",
"description": "Font loader optimized for maximum performance. Removes render-blocking font resources and loads them asynchronusly. Handle FOUT & FOUC with font loading status watcher. Supports both local-hosted fonts and web fonts.",
"keywords": [
"gatsby-plugin",
@@ -27,7 +27,7 @@
"url": "https://github.com/codeAdrian/gatsby-omni-font-loader"
},
"peerDependencies": {
- "gatsby": "^2.0.0 || ^3.0.0",
+ "gatsby": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
"react-helmet": ">=6.0.0"
}
}
diff --git a/utils/fontListener.ts b/utils/fontListener.ts
new file mode 100644
index 0000000..0b7e719
--- /dev/null
+++ b/utils/fontListener.ts
@@ -0,0 +1,54 @@
+import { kebabCase } from "../utils";
+
+declare var document: { fonts: any };
+
+export const fontListener = ({ fontNames, scope }) => {
+ const hasFonts = fontNames && Boolean(fontNames.length);
+ const targetElement = scope === "html" ? "documentElement" : "body";
+ const apiAvailable = "fonts" in document;
+
+ function handleLoadComplete() {
+ addClassName("all");
+ }
+
+ function handleFontLoad(fontFaces: FontFace[]) {
+ fontFaces.forEach((fontFace) => {
+ addClassName(fontFace.family);
+ })
+ }
+
+ function fontMapper(fontName) {
+ return document.fonts
+ .load(`1rem ${fontName}`)
+ .then(handleFontLoad)
+ .catch(errorFallback);
+ }
+
+ function loadFonts() {
+ const fonts = fontNames.map(fontMapper);
+ Promise.all(fonts).then(handleLoadComplete).catch(errorFallback);
+ }
+
+ function errorFallback() {
+ fontNames.forEach(addClassName);
+ }
+
+ function handleApiError(error) {
+ console.info(`document.fonts API error: ${error}`);
+ console.info(`Replacing fonts instantly. FOUT handling failed.`);
+ errorFallback();
+ }
+
+ function addClassName(fontName: string) {
+ document[targetElement].classList.add(`wf-${kebabCase(fontName)}`);
+ }
+
+ if (!apiAvailable) {
+ handleApiError("Font loading API not available");
+ return;
+ }
+
+ if (hasFonts && apiAvailable) {
+ loadFonts();
+ }
+};