squidfunk/mkdocs-material · error · PluginError

Couldn't find font family

Error message

Couldn't find font family '{family}' on Google Fonts ({res.status_code}: {res.reason})

What it means

`_fetch_font_from_google_fonts` downloads the font manifest from `https://fonts.google.com/download/list?family=...` when resolving a font family for social cards. Any non-200 response (font not found, rate limiting, network/proxy failure) raises this PluginError with the HTTP status and reason.

Solutions

  1. Verify the font family name exists on fonts.google.com and use its exact name (spaces encoded).
  2. For custom/local fonts, place the font files in the cache directory yourself or use a layout that references a bundled font instead of Google Fonts.
  3. Fix network egress: configure HTTPS_PROXY, allow fonts.google.com in the firewall, or pre-populate the plugin's font cache.
  4. Retry the build if the failure was a transient 429/5xx; add caching of downloaded fonts between CI runs.

Example fix

# before (mkdocs.yml, font not on Google Fonts)
- social:
    cards_font: My Corporate Sans

# after: use a Google-hosted family or bundle locally
- social:
    cards_font: Roboto
Defensive patterns

Strategy: retry

Validate before calling

# preflight: fonts reachable and family known
def font_available(family):
    import requests
    r = requests.head(f"https://fonts.google.com/download/list?family={family}", timeout=10)
    return r.status_code == 200

Try / catch

from mkdocs.exceptions import PluginError
import time
for attempt in range(3):
    try:
        return plugin._resolve_font(family, style)
    except PluginError as e:
        if ' 429' in str(e) or ' 50' in str(e):
            time.sleep(2 ** attempt)
            continue
        raise

Prevention

When it happens

Trigger: A card layout requests a font family not available on Google Fonts; no network access or a blocking proxy/firewall; Google Fonts returns 403/429 (rate limit or bot protection); family name misspelled or using a non-Google-Fonts font.

Common situations: CI environments without internet egress; corporate proxies blocking fonts.google.com; custom `cards_font` set to a self-hosted/local font that Google doesn't serve; transient 429s during batch builds.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


AI-assisted analysis of squidfunk/mkdocs-material@e2136532f4 (2026-08-29). Data as JSON: /api/errors/4df56e8d0c95830d. Report an issue: GitHub.

Appendix: source

Thrown at src/plugins/social/plugin.py:890

        # Fall back to regular font (guess if there are multiple)
        return self._resolve_font(family, fallback)

    # -------------------------------------------------------------------------

    # Fetch font family from Google Fonts
    def _fetch_font_from_google_fonts(self, family: str):
        path = os.path.join(self.config.cache_dir, "fonts")

        # Download manifest from Google Fonts - Google returns JSON with syntax
        # errors, so we just treat the response as plain text and parse out all
        # URLs to font files, as we're going to rename them anyway. This should
        # be more resilient than trying to correct the JSON syntax.
        url = f"https://fonts.google.com/download/list?family={family}"
        res = requests.get(url)

        # Ensure that the download succeeded
        if res.status_code != 200:
            raise PluginError(
                f"Couldn't find font family '{family}' on Google Fonts "
                f"({res.status_code}: {res.reason})"
            )

        # Extract font URLs from manifest
        for match in re.findall(
            r"\"(https:(?:.*?)\.[ot]tf)\"", str(res.content)
        ):
            with requests.get(match) as res:
                res.raise_for_status()

                # Construct image font for analysis by directly reading the
                # contents from the response without priorily writing to a
                # temporary file (like we did before), as this might lead to
                # problems on Windows machines, see https://t.ly/LiF_k
                with BytesIO(res.content) as f:
                    font = ImageFont.truetype(f)

View on GitHub (pinned to e2136532f4)