Search code examples
cssreactjsfontswebpack-4

How to correctly load .otf/.tff fonts in a React app via a Webpack pipeline?


I'm having a hell of a time making this happen and it's possible the various guide I've been following are outdated or I messed something up. I know this has been a common question, and I've played with awesome font, loading it via scss, etc... and it hasn't worked, plus I found it overly complicated where the below approach is also supposed to work and more straight forward.

The webpack error (at the bottom) is thrown by css-loader which isn't the file-loader or url-loader (I tried both), so I suspect the problem is the css-loader is trying to import/find by .otf font file and can't.

If anyone has a clue, it would really help. I need to pack in the font file so my app can run offline.

Here's my file structure:

./webkacp.config.js
./src/
   ./fonts/AvenirLTStd-Roman.otf
   ./index.css
   ./index.jsx

Here's my webpack.config.js:

var HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

module.exports = {
    mode: 'development',
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                loader: 'babel-loader'
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader'],
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'fonts/'
                        }
                    }
                ],
            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/'
                        }
                    }
                ],
            }
        ]
    },
    resolve: {
        extensions: ['.js', '.jsx'],
        alias: {
            '@': path.resolve(__dirname, 'src/'),
        }
    },
    plugins: [new HtmlWebpackPlugin({
        template: './src/index.html'
    })],
    devServer: {
        historyApiFallback: true
    },
    externals: {
        // global app config object
        config: JSON.stringify({
            apiUrl: 'http://127.0.0.1:4000'
        })
    }
}

Here's my index.css:

@font-face {
font-family: "MyFont";
src: url("./fonts/AvenirLTStd-Roman.otf") format("otf");
/* Add other formats as you see fit */
}

html, body {
    font-family: 'MyFont', sans-serif;
}

Here's my index.jsx:

import React from 'react';
import { render } from 'react-dom';

import { App } from './App';

import 'bootstrap/dist/css/bootstrap.min.css';
import '@/index.css';

render(
    <App />,
    document.getElementById('app')
);

Finally, here's the webpack error:

ERROR in ./src/index.css (./node_modules/css-loader/dist/cjs.js!./src/index.css)
Module not found: Error: Can't resolve './fonts/AvenirLTStd-Roman.otf' in '/var/sphyrna/csdpac-services/images/csdpac-unified/frontend/src'
 @ ./src/index.css (./node_modules/css-loader/dist/cjs.js!./src/index.css) 4:36-76
 @ ./src/index.css
 @ ./src/index.jsx

Solution

  • Got it.

    Two changes were needed:

    1. Skip the urls in css-loader so it doesn't try to import/resolve the file.
    2. Remove the san-serif in my css.

    Works now. Hope this helps someone else. None of the online guides mention skipping the url.

    Here's my updated css:

    @font-face {
    font-family: "MyFont";
    src: url("./fonts/AvenirLTStd-Roman.otf") format("otf");
    /* Add other formats as you see fit */
    }
    
    html, body {
        font-family: 'MyFont';
    }
    

    The Webpack.config.js:

    var HtmlWebpackPlugin = require('html-webpack-plugin');
    const path = require('path');
    
    module.exports = {
        mode: 'development',
        module: {
            rules: [
                {
                    test: /\.jsx?$/,
                    loader: 'babel-loader'
                },
                {
                    test: /\.css$/,
                    use: [
                        {
                            loader: 'style-loader'
                        },
                        {
                            loader: 'css-loader',
                            options: {url:false}
                        }
                    ]
                },
                {
                    test: /\.(woff|woff2|eot|ttf|otf)$/,
                    use: [
                        {
                            loader: 'url-loader',
                            options: {
                                name: '[name].[ext]',
                                outputPath: 'fonts/'
                            }
                        }
                    ],
                },
                {
                    test: /\.(png|svg|jpg|gif)$/,
                    use: [
                        {
                            loader: 'file-loader',
                            options: {
                                name: '[name].[ext]',
                                outputPath: 'images/'
                            }
                        }
                    ],
                }
            ]
        },
        resolve: {
            extensions: ['.js', '.jsx'],
            alias: {
                '@': path.resolve(__dirname, 'src/'),
            }
        },
        plugins: [new HtmlWebpackPlugin({
            template: './src/index.html'
        })],
        devServer: {
            historyApiFallback: true
        },
        externals: {
            // global app config object
            config: JSON.stringify({
                apiUrl: 'http://127.0.0.1:4000'
            })
        }
    }