nuxt / fonts

Plug-and-play web font optimization and configuration for Nuxt apps.
https://fonts.nuxt.com
MIT License
424 stars 7 forks source link

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

Closed JulianDueck closed 1 week ago

JulianDueck commented 3 months ago

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

dosubot[bot] commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

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

github-actions[bot] commented 2 weeks ago

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](https://nuxt.com/docs/community/reporting-bugs/#create-a-minimal-reproduction). You might also find these other articles interesting and/or helpful: - [The Importance of Reproductions](https://antfu.me/posts/why-reproductions-are-required) - [How to Generate a Minimal, Complete, and Verifiable Example](https://stackoverflow.com/help/mcve)
github-actions[bot] commented 1 week ago

This issue was closed because it was open for 7 days without a reproduction.