Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Font not loading when upgrading to nuxt 3.12.1 and enabling compatibilityVersion: 4 #176

Open
JulianDueck opened this issue Jun 11, 2024 · 4 comments

Comments

@JulianDueck
Copy link

Hello.
I upgraded my project to nuxt 3.12.1 with npx nuxi@latest upgrade --force.
Enabled this flag in nuxt.config.ts

export default defineNuxtConfig({
  future: {
    compatibilityVersion: 4,
  },
})

Restructured my projects file structure following this guide.
My app functions like before, only that my custom font is not loading. I have a .woff font in public/ folder, but nuxt is not using it for some reason. Before the changes I made, it worked just fine.
I'm using nuxt/fonts 0.7.0

Copy link

dosubot bot commented Jun 11, 2024

Hey there, @JulianDueck! I'm here to help you with any issues you're facing in your project. Feel free to ask me anything while you wait for a human maintainer to join the conversation.

To resolve the issue of your custom font not loading after upgrading to Nuxt 3.12.1 and restructuring your project's file structure, ensure that the path to your custom font is correct and that the font file is located in the appropriate directory.

Based on the provided nuxt.config.ts file, your custom font MyCustom is expected to be located at /public/custom-font.woff2 and CustomGlobal at /public/font-global.woff2. Make sure these font files are indeed in the public directory of your project.

Here is the relevant part of your nuxt.config.ts:

export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: ['@nuxt/fonts', '@nuxtjs/tailwindcss', '@unocss/nuxt'],
  unocss: {
    disableNuxtInlineStyle: false,
  },
  fonts: {
    providers: {
      custom: '~/providers/custom',
    },
    families: [
      { name: 'MyCustom', src: '/custom-font.woff2' },
      { name: 'CustomGlobal', global: true, src: '/font-global.woff2' },
    ],
  },
})

Ensure that the font files are correctly placed in the public directory:

your-project/
├── public/
│   ├── custom-font.woff2
│   └── font-global.woff2
├── nuxt.config.ts
└── ...

Additionally, the src/module.ts file in the Nuxt fonts module handles the setup and resolution of font providers. Ensure that your custom fonts are being correctly registered by this module. Here is a snippet from src/module.ts:

import { addBuildPlugin, addTemplate, defineNuxtModule, resolveAlias, resolvePath, useNuxt } from '@nuxt/kit'
import jiti from 'jiti'
import type { ResourceMeta } from 'vue-bundle-renderer'
import { join, relative } from 'pathe'

import { withoutLeadingSlash } from 'ufo'
import local from './providers/local'
import google from './providers/google'
import bunny from './providers/bunny'
import fontshare from './providers/fontshare'
import adobe from './providers/adobe'
import fontsource from './providers/fontsource'

import { FontFamilyInjectionPlugin, type FontFaceResolution } from './plugins/transform'
import { generateFontFace } from './css/render'
import type { GenericCSSFamily } from './css/parse'
import { setupPublicAssetStrategy } from './assets'
import type { FontFamilyManualOverride, FontFamilyProviderOverride, FontProvider, ModuleHooks, ModuleOptions } from './types'
import { setupDevtoolsConnection } from './devtools'
import { logger } from './logger'

export default defineNuxtModule<ModuleOptions>({
  meta: {
    name: '@nuxt/fonts',
    configKey: 'fonts',
  },
  defaults: {
    devtools: true,
    experimental: {
      processCSSVariables: false,
    },
    defaults: {},
    assets: {
      prefix: '/_fonts',
    },
    local: {},
    google: {},
    adobe: {
      id: '',
    },
    providers: {
      local,
      adobe,
      google,
      bunny,
      fontshare,
      fontsource,
    },
  },
  async setup(options, nuxt) {
    // Skip when preparing
    if (nuxt.options._prepare) return

    // Custom merging for defaults - providing a value for any default will override module
    // defaults entirely (to prevent array merging)
    const normalizedDefaults = {
      weights: (options.defaults?.weights || defaultValues.weights).map(v => String(v)),
      styles: options.defaults?.styles || defaultValues.styles,
      subsets: options.defaults?.subsets || defaultValues.subsets,
      fallbacks: Object.fromEntries(Object.entries(defaultValues.fallbacks).map(([key, value]) => [
        key,
        Array.isArray(options.defaults?.fallbacks) ? options.defaults.fallbacks : options.defaults?.fallbacks?.[key as GenericCSSFamily] || value,
      ])) as Record<GenericCSSFamily, string[]>,
    }

    if (!options.defaults?.fallbacks || !Array.isArray(options.defaults.fallbacks)) {
      const fallbacks = (options.defaults!.fallbacks as Exclude<NonNullable<typeof options.defaults>['fallbacks'], string[]>) ||= {}
      for (const _key in defaultValues.fallbacks) {
        const key = _key as keyof typeof defaultValues.fallbacks
        fallbacks[key] ||= defaultValues.fallbacks[key]
      }
    }

    const providers = await resolveProviders(options.providers)
    const prioritisedProviders = new Set<string>()
    // Allow registering and disabling providers
    nuxt.hook('modules:done', async () => {
      await nuxt.callHook('fonts:providers', providers)
      const setups: Array<void | Promise<void>> = []
      for (const key in providers) {
        const provider = providers[key]!
        if (options.providers?.[key] === false || (options.provider && options.provider !== key)) {
          // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
          delete providers[key]
        }
        else if (provider.setup) {
          setups.push(provider.setup(options[key as 'google' | 'local' | 'adobe'] || {}, nuxt))
        }
      }
      await Promise.all(setups)
      for (const val of options.priority || []) {
        if (val in providers) prioritisedProviders.add(val)
      }
      for (const provider in providers) {
        prioritisedProviders.add(provider)
      }
    })

    const { normalizeFontData } = setupPublicAssetStrategy(options.assets)
    const { exposeFont } = setupDevtoolsConnection(nuxt.options.dev && !!options.devtools)

    async function resolveFontFaceWithOverride(fontFamily: string, override?: FontFamilyManualOverride | FontFamilyProviderOverride, fallbackOptions?: { fallbacks: string[], generic?: GenericCSSFamily }): Promise<FontFaceResolution | undefined> {
      const fallbacks = override?.fallbacks || normalizedDefaults.fallbacks[fallbackOptions?.generic || 'sans-serif']

      if (override && 'src' in override) {
        const fonts = normalizeFontData({
          src: override.src,
          display: override.display,
          weight: override.weight,
          style: override.style,
        })
        exposeFont({
          type: 'manual',
          fontFamily,
          fonts,
        })
        return {
          fallbacks,
          fonts,
        }
      }

      // Respect fonts that should not be resolved through `@nuxt/fonts`
      if (override?.provider === 'none') {
        return
      }

      // Respect custom weights, styles and subsets options
      const defaults = { ...normalizedDefaults, fallbacks }
      for (const key of ['weights', 'styles', 'subsets'] as const) {
        if (override?.[key]) {
          defaults[key as 'weights'] = override[key]!.map(v => String(v))
        }
      }

      // Handle explicit provider
      if (override?.provider) {
        if (override.provider in providers) {
          const result = await providers[override.provider]!.resolveFontFaces!(fontFamily, defaults)
          // Rewrite font source URLs to be proxied/local URLs
          const fonts = normalizeFontData(result?.fonts || [])
          if (!fonts.length || !result) {
            logger.warn(`Could not produce font face declaration from \`${override.provider}\` for font family \`${fontFamily}\`.`)
            return
          }
          exposeFont({
            type: 'override',
            fontFamily,
            provider: override.provider,
            fonts,
          })
          return {
            fallbacks: result.fallbacks || defaults.fallbacks,
            fonts,
          }
        }

        // If not registered, log and fall back to default providers
        logger.warn(`Unknown provider \`${override.provider}\` for font family \`${fontFamily}\`. Falling back to default providers.`)
      }

      for (const key of prioritisedProviders) {
        const provider = providers[key]!
        if (provider.resolveFontFaces) {
          const result = await provider.resolveFontFaces(fontFamily, defaults)
          if (result) {
            // Rewrite font source URLs to be proxied/local URLs
            const fonts = normalizeFontData(result.fonts)
            if (fonts.length > 0) {
              exposeFont({
                type: 'auto',
                fontFamily,
                provider: key,
                fonts,
              })
              return {
                fallbacks: result.fallbacks || defaults.fallbacks,
                fonts,
              }
            }
            if (override) {
              logger.warn(`Could not produce font face declaration for \`${fontFamily}\` with override.`)
            }
          }
        }
      }
    }

    nuxt.options.css.push('#build/nuxt-fonts-global.css')
    addTemplate({
      filename: 'nuxt-fonts-global.css',
      write: true, // Seemingly necessary to allow vite to process file 🤔
      async getContents() {
        let css = ''
        for (const family of options.families || []) {
          if (!family.global) continue
          const result = await resolveFontFaceWithOverride(family.name, family)
          for (const font of result?.fonts || []) {
            // We only inject basic `@font-face` as metrics for fallbacks don't make sense
            // in this context unless we provide a name for the user to use elsewhere as a
            // `font-family`.
            css += generateFontFace(family.name, font) + '\n'
          }
        }
        return css
      },
    })

    const fontMap = new Map<string, Set<string>>()
    nuxt.hook('build:manifest', (manifest) => {
      function addPreloadLinks(chunk: ResourceMeta, urls: Set<string>) {
        chunk.assets ||= []
        for (const url of urls) {
          chunk.assets.push(url)
          if (!manifest[url]) {
            manifest[url] = {
              file: relative(nuxt.options.app.buildAssetsDir, url),
              resourceType: 'font',
              preload: true,
            }
          }
        }
      }

      // CSS files in bundle
      for (const id in manifest) {
        const chunk = manifest[id]!
        if (!chunk.css || chunk.css.length === 0) continue
        for (const css of chunk.css) {
          const assetName = withoutLeadingSlash(join(nuxt.options.app.buildAssetsDir, css))
          if (fontMap.has(assetName)) {
            addPreloadLinks(chunk, fontMap.get(assetName)!)
          }
        }
      }

      // Source files in bundle
      for (const [id, urls] of fontMap) {
        const chunk = manifest[relative(nuxt.options.srcDir, id)]
        if (!chunk) continue

        addPreloadLinks(chunk, urls)
      }
    })

    addBuildPlugin(FontFamilyInjectionPlugin({
      dev: nuxt.options.dev,
      fontsToPreload: fontMap,
      processCSSVariables: options.experimental?.processCSSVariables,
      shouldPreload(fontFamily, fontFace) {
        const override = options.families?.find(f => f.name === fontFamily)
        if (override && override.preload !== undefined) {
          return override.preload
        }
        if (options.defaults?.preload !== undefined) {
          return options.defaults.preload
        }
        return fontFace.src.some(s => 'url' in s) && !fontFace.unicodeRange
      },
      async resolveFontFace(fontFamily, fallbackOptions) {
        const override = options.families?.find(f => f.name === fontFamily)

        // This CSS will be injected in a separate location
        if (override?.global) {
          return
        }
        return resolveFontFaceWithOverride(fontFamily, override, fallbackOptions)
      },
    }))
  },
})

async function resolveProviders(_providers: ModuleOptions['providers'] = {}) {
  const nuxt = useNuxt()
  const _jiti = jiti(nuxt.options.rootDir, { interopDefault: true })

  const providers = { ..._providers }
  for (const key in providers) {
    const value = providers[key]
    if (value === false) {
      // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
      delete providers[key]
    }
    if (typeof value === 'string') {
      providers[key] = await _jiti(await resolvePath(resolveAlias(value)))
    }
  }
  return providers as Record<string, FontProvider>
}

declare module '@nuxt/schema' {
  interface NuxtHooks extends ModuleHooks {}
}

If the font files are correctly placed and the issue persists, try clearing the cache and rebuilding your project:

npm run clean
npm run build
npm run start

This should resolve the issue of your custom font not loading [1][2][3].

To continue talking to Dosu, mention @dosu.

@oritwoen
Copy link

oritwoen commented Jul 2, 2024

I have the same error. But it only occurs in my development environment. Once built, everything works fine.

The path /_fonts/* returns endpoint 404 to dev.

Zrzut ekranu z 2024-07-02 18-41-54

@vptill
Copy link

vptill commented Jul 3, 2024

It never worked for me in development mode, however, after updating to 3.12.1, it won't load in production either.

Copy link

Would you be able to provide a reproduction? 🙏

More info

Why do I need to provide a reproduction?

Reproductions make it possible for us to triage and fix issues quickly with a relatively small team. It helps us discover the source of the problem, and also can reveal assumptions you or we might be making.

What will happen?

If you've provided a reproduction, we'll remove the label and try to reproduce the issue. If we can, we'll mark it as a bug and prioritise it based on its severity and how many people we think it might affect.

If needs reproduction labeled issues don't receive any substantial activity (e.g., new comments featuring a reproduction link), we'll close them. That's not because we don't care! At any point, feel free to comment with a reproduction and we'll reopen it.

How can I create a reproduction?

We have a template for starting with a minimal reproduction:

👉 https://stackblitz.com/github/nuxt/fonts/tree/main/example

A public GitHub repository is also perfect. 👌

Please ensure that the reproduction is as minimal as possible. See more details in our guide.

You might also find these other articles interesting and/or helpful:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants