---
title: "Font Optimization for Astro websites"
description: "Find the roadblocks and checkpoints by the maker of astro-font, the font optimization library by LaunchFa.st for Astro websites."
source: "https://www.launchfa.st/blog/building-astro-font"
author: "Rishi Raj Jain"
created_at: 2024-01-17T00:00:00.000Z
---

> **Site index for agents**
> Fetch https://www.launchfa.st/llms.txt to discover every page on this site.
> Every page is also available as Markdown by appending `.md` to its URL.

<img width="632" height="580" alt="astro-font by LaunchFa.st" loading="eager" class="mt-4 border rounded-sm bg-cover bg-center bg-no-repeat transform will-change-auto" src="https://pbs.twimg.com/media/GEC7gffWUAA0HJZ?format=png&name=small" />

In ~1 month, `astro-font` has grown to 5.7K downloads 🤯

## Checkpoint #1

It all began with me thinking about what was missing from the Astro ecosystem. After checking out some of the websites (including my own hybrid Astro website, [launchfa.st](https://www.launchfa.st)), I found that an end-to-end font optimization library was missing 👇

## Checkpoint #2

So, I actually set out to build an Astro package that'll deliver the promise. I thought it'd be super easy to just use the scripts @vercel's `next/font` does and ship it (and I did that very initially!). It worked great for static websites! BUT, enter SSR websites👇

## Roadblock #1

`next/font` being coupled with the build process of Next has access to the output directories and the expected runtime configuration and thus it self-hosts fonts even in SSR-first websites. This space got more complex for Astro SSR websites as `astro-font` is an Astro component and not an Astro integration! Here's what I did to solve that problem 👇

```javascript
async function getOS(): Promise<typeof import('node:os') | undefined> {
  let os
  try {
    os = await import('node:os')
    return os
  } catch (e) {}
}

// Check if writing is permitted by the file system
async function ifFSOSWrites(dir: string): Promise<string | undefined> {
  try {
    const fs = await getFS()
    if (fs) {
      const testDir = join(dir, '.astro_font')
      if (!fs.existsSync(testDir)) fs.mkdirSync(testDir)
      fs.rmSync(testDir, { recursive: true, force: true })
      return dir
    }
  } catch (e) {}
}
```

## Roadblock #2

Great! That worked and allowed me to determine if the SSR build had fonts shipped with it and thus allowing me to compute the fallback font at the runtime. BUT, some users wanted to use CDN URLs or were using fontsource fonts. There was no way to possibly know to what [Vite](https://vitejs.dev) resolved the internal fonts to. Hence, I built a runtime Google Fonts-like CSS parser 👇

```javascript
// Custom script to parseGoogleCSS
function parseGoogleCSS(tmp: string) {
  let match
  const fontFaceMatches = []
  const fontFaceRegex = /@font-face\s*{([^}]+)}/g
  while ((match = fontFaceRegex.exec(tmp)) !== null) {
    const fontFaceRule = match[1]
    const fontFaceObject: any = {}
    fontFaceRule.split(';').forEach((property) => {
      if (property.includes('src: ')) {
        const formatPosition = property.indexOf('for')
        fontFaceObject['path'] = property
          .trim()
          .substring(9, formatPosition ? formatPosition - 5 : property.length - 1)
          .trim()
      }
      if (property.includes('-style: ')) {
        fontFaceObject['style'] = property.split(':').map((i) => i.trim())[1]
      }
      if (property.includes('-weight: ')) {
        fontFaceObject['weight'] = property.split(':').map((i) => i.trim())[1]
      }
      if (property.includes('unicode-range: ')) {
        if (!fontFaceObject['css']) {
          fontFaceObject['css'] = {}
        }
        fontFaceObject['css']['unicode-range'] = property.split(':').map((i) => i.trim())[1]
      }
    })
    fontFaceMatches.push(fontFaceObject)
  }
  return fontFaceMatches
}
```

## Checkpoint #3

Seems complete, right? It now works with local fonts and fonts over CDN. But runtime fetch + compute is gonna cost us SSR time. To solve that, enter runtime font caching 👇

```javascript
const [os, fs] = await Promise.all([getOS(), getFS()])
if (fs) {
    if (os) {
      writeAllowed = await Promise.all([ifFSOSWrites(os.tmpdir()), ifFSOSWrites('/tmp')])
      tmpDir = writeAllowed.find((i) => i !== undefined)
      cacheDir = fontCollection.cacheDir || tmpDir
      if (cacheDir) {
        // Create a json based on slugified path, style and weight
        const slugifyPath = (i: Source) => `${i.path}_${i.style}_${i.weight}`
        const slugifiedCollection = fontCollection.src.map(slugifyPath)
        const cachedFileName = simpleHash(slugifiedCollection.join('_')) + '.txt'
        cachedFilePath = join(cacheDir, cachedFileName)
        if (fs.existsSync(cachedFilePath)) {
          try {
            const tmpCachedFilePath = fs.readFileSync(cachedFilePath, 'utf8')
            return JSON.parse(tmpCachedFilePath)
          } catch (errorReadingCache) {}
        }
      }
    }
}
```

## Checkpoint #4

Now? We're just left with one thing to do. Allow per font per config preloads (and backward support to global preloads (not))!

```javascript
// If the parent preload is set to be false, look for true only preload values
if (fontCollection.preload === false) {
    return fontCollection.src
        .filter((i) => i.preload === true)
        .map((i) => getRelativePath(getBasePath(fontCollection.basePath), i.path))
}

// If the parent preload is set to be true (or not defined), look for non-false values
return fontCollection.src
    .filter((i) => i.preload !== false)
    .map((i) => getRelativePath(getBasePath(fontCollection.basePath), i.path))
```

And we're done, and it's live in production for [many Astro websites](/blog/astro-font-showcase) ✨

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Thank you for that awesome package... it helped me fight the &quot;layout shift&quot;!<br>I will add it to Best of JS, as we have a tag `Astro` <a href="https://t.co/38Akqa5IQd">https://t.co/38Akqa5IQd</a></p>&mdash; Michael Rambeau (@michaelrambeau) <a href="https://twitter.com/michaelrambeau/status/1747612731728179669?ref_src=twsrc%5Etfw">January 17, 2024</a></blockquote> <script defer src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<div class="columns-2"> <img alt="Astro Font development screenshot" loading="lazy" class="mt-8 w-full rounded-sm" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/Screenshot%202024-01-11%20at%2011.36.59%E2%80%AFAM.png" ><img alt="Astro Font development screenshot" loading="lazy" class="mt-8 w-full rounded-sm" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/Screenshot%202024-01-18%20at%208.39.40%E2%80%AFPM.png" ><img alt="Astro Font development screenshot" loading="lazy" class="mt-8 w-full rounded-sm" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/Screenshot%202024-01-18%20at%208.40.02%E2%80%AFPM.png" ><img alt="Astro Font development screenshot" loading="lazy" class="mt-8 w-full rounded-sm" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/Screenshot%202024-01-18%20at%208.37.44%E2%80%AFPM.png" ><img alt="Astro Font development screenshot" loading="lazy" class="mt-8 w-full rounded-sm" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/Screenshot%202024-01-18%20at%2010.07.00%E2%80%AFAM.png" ><img alt="Astro Font development screenshot" loading="lazy" class="mt-8 w-full rounded-sm" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/Screenshot%202024-01-18%20at%208.38.50%E2%80%AFPM.png" ><img alt="Astro Font development screenshot" loading="lazy" class="mt-8 w-full rounded-sm" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/Screenshot%202024-01-18%20at%208.35.19%E2%80%AFPM.png" ><img alt="Astro Font development screenshot" loading="lazy" class="mt-8 w-full rounded-sm" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/Screenshot%202024-01-18%20at%208.36.25%E2%80%AFPM.png" ><img alt="Astro Font development screenshot" loading="lazy" class="mt-8 w-full rounded-sm" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/Screenshot%202024-01-18%20at%208.36.58%E2%80%AFPM.png" ><img alt="Astro Font development screenshot" loading="lazy" class="mt-8 w-full rounded-sm" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/Screenshot%202024-01-18%20at%208.39.20%E2%80%AFPM.png" ><img alt="Astro Font development screenshot" loading="lazy" class="mt-8 w-full rounded-sm" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/Screenshot%202024-01-18%20at%208.38.35%E2%80%AFPM.png"> </div>
