Favicon

Publish at:

Favicon formats come in different shapes and sizes. Describing all of them is outside the scope of this writing. It is important to note that different devices have different requirements for how and what to represent as a favicon.

As far as webpack is concerned, all favicon formats can be split into two groups:

  • a format that can be served as‑is from the root of the website by being referenced in HTML
  • a format that is not referenced directly from HTML and requires additional metadata to describe how the device can display it

In any case, a favicon is just an asset that can be stored in source and copied to the destination during build time, or it can be generated on the fly first. Copying the file to the destination is taken care of by a loader or plugin, for example file-loader or copy-webpack-plugin.

There are webpack plugins for generating a set of favicons during build time. They provide options to optimize their work (minimizing build time) via caching and other mechanisms. But is it really necessary to generate all favicons on every build? We will look at a slightly different approach that generates favicons on demand (independent of the webpack build) and saves them into the source folder. Later, any appropriate loader or plugin can pick them up and move them to the destination folder. This approach requires more implementation, but it also exposes new ways of dealing with assets.

Dependencies #

Following the idea described above, we are going to use as few dependencies as possible. In particular:

  • favicons - generates favicon assets based on a config
  • copy-webpack-plugin - webpack plugin that copies files from source to destination via the webpack pipeline
  • @types/copy-webpack-plugin - copy-webpack-plugin type definitions
  • del - folders/files clean up utility

Because we are generating favicon assets on demand, we don't need the favicons package as a dependency in our main app (otherwise it will be restored in every CI build). For that reason, we can create another Node.js–based project just for favicon generation. We are going to create that project as a subfolder inside our root repository.

mkdir favicon-generator
cd favicon-generator
npm init -y
npm i favicons -D -save-exact

Because our source code consists mostly of TypeScript, let's add language support as well:

npm i cross-env del typescript ts-node -D --save-exact

Let's add dependencies to the main app:

cd ..
npm i copy-webpack-plugin @types/copy-webpack-plugin -D --save-exact

Favicon generator #

The bulk of the work will be in the favicon-generator folder. First, we need to understand how the favicons package works. Given a config object and a path to the source image, it generates all the metadata needed for the set of favicons to be used. The metadata includes HTML code to be inserted into the index.html file, binary streams for all the generated image files, and the manifest file. The file generator needs to take all that information and put it into the corresponding places within the project structure so webpack can pick them up and transfer them to the destination folder.

The file generator work will consist of three main parts: generate data, clean up previously generated information (if any), and save generated data.

favicon-generator/index.ts #

Create favicon-generator/index.ts file:

import { generate } from './utils/generate-data'
import { deleteFiles } from './utils/delete-files'
import { save } from './utils/save-data'

const run = async () => {
    const data = await generate()
    await deleteFiles()
    await save(data)
}

run()

Create utils folder:

mkdir utils

favicon-generator/config.ts #

Create favicon-generator/config.ts file:

export const configuration = {
    source: '../assets/images/logo1.png',       // Path to the image file to generate favicons from
    destination: '../assets/favicons',          // Folder path to save generated assets into
    html: {
        source: '../webpack/index.html',        // Path to HTML file to insert link to generated assets
        marker: {
            start: '<!-- <favicon> -->',        // Regex as string to mark HTML snippet starting point for links substitution
            end: '<!-- <\/favicon> -->'         // Regex as string to mark HTML snippet end point for links substitution
        }
    },

    favicons: {
        path: "/",                                // Path for overriding default icons path. `string`
        appName: null,                            // Your application's name. `string`
        appShortName: null,                       // Your application's short_name. `string`. Optional. If not set, appName will be used
        appDescription: null,                     // Your application's description. `string`
        developerName: null,                      // Your (or your developer's) name. `string`
        developerURL: null,                       // Your (or your developer's) URL. `string`
        dir: "auto",                              // Primary text direction for name, short_name, and description
        lang: "en-US",                            // Primary language for name and short_name
        background: "#fff",                       // Background colour for flattened icons. `string`
        theme_color: "#fff",                      // Theme color user for example in Android's task switcher. `string`
        appleStatusBarStyle: "black-translucent", // Style for Apple status bar: "black-translucent", "default", "black". `string`
        display: "standalone",                    // Preferred display mode: "fullscreen", "standalone", "minimal-ui" or "browser". `string`
        orientation: "any",                       // Default orientation: "any", "natural", "portrait" or "landscape". `string`
        scope: "/",                               // set of URLs that the browser considers within your app
        start_url: "/?homescreen=1",              // Start URL when launching the application from a device. `string`
        version: "1.0",                           // Your application's version string. `string`
        logging: false,                           // Print logs to console? `boolean`
        pixel_art: false,                         // Keeps pixels "sharp" when scaling up, for pixel art.  Only supported in offline mode.
        loadManifestWithCredentials: false,       // Browsers don't send cookies when fetching a manifest, enable this to fix that. `boolean`
        icons: {
            // Platform Options:
            // - offset - offset in percentage
            // - background:
            //   * false - use default
            //   * true - force use default, e.g. set background for Android icons
            //   * color - set background for the specified icons
            //   * mask - apply mask in order to create circle icon (applied by default for firefox). `boolean`
            //   * overlayGlow - apply glow effect after mask has been applied (applied by default for firefox). `boolean`
            //   * overlayShadow - apply drop shadow after mask has been applied .`boolean`
            //
            android: false,              // Create Android homescreen icon. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }`
            appleIcon: false,            // Create Apple touch icons. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }`
            appleStartup: false,         // Create Apple startup images. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }`
            coast: false,                // Create Opera Coast icon. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }`
            favicons: true,             // Create regular favicons. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }`
            firefox: false,              // Create Firefox OS icons. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }`
            windows: false,              // Create Windows 8 tile icons. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }`
            yandex: false                // Create Yandex browser icon. `boolean` or `{ offset, background, mask, overlayGlow, overlayShadow }`
        }
    }
}

All paths in the config file are relative to favicon-generator folder

Config file consist of 3 sections:

  • source - relative path for the source image file to be used for favicon generation
  • destination - relative path to location for saving generated assets into
  • html - provides details of index.html file manipulation (relative path and substitution points)

Generate data #

favicon-generator/utils/common.ts #

Create favicon-generator/utils/common.ts file:

export type Result = {
    images: { name: string, contents: Buffer }[],
    files: { name: string, contents: string }[],
    html: string[]
}

Result type represents end data structure generated by favicons package:

  • images - array of name - Buffer pairs of generated assets
  • files - array of name - contents pairs of manifest files (json format)
  • html - array of HTML links to be inserted into index.html file

favicon-generator/utils/generate-data.ts #

Create favicon-generator/utils/generate-data.ts file:

import util from 'util'
const favicons = require('favicons')
import { Result } from './common'
import { configuration } from '../config'

const favIcons: (source: string, configuration: {}) => Promise<Result>
    = util.promisify(favicons)

export const generate = async () => {
    const data = await favIcons(configuration.source, configuration.favicons)

    data.html.sort((a, b) => {
        const aValue = a.substring(0, 5)
        const bValue = b.substring(0, 5)

        return aValue < bValue ? 1 : (aValue > bValue ? -1 : 0)
    })

    data.html = data.html
    .map((item) => {
        const matches = item.match(/href.*=.*"(?<value>.*?)"/)

        if(!matches || !matches.groups || !matches.groups['value'] ||
            matches.groups['value'].endsWith('.json')) {
            return item
        }

        const replacement = `href="${matches.groups['value']}"`;
        return item.replace(/href.*=.*".*"/, replacement)
    })

    return data
}

The main idea of this file is to convert what was generated by the favicons package into information that can be plugged into the webpack pipeline. We don't need to do anything with images and files; those assets will be copied into the appropriate places in the source tree. But the HTML is different. Effectively, we are getting a list of HTML link tags with href attributes pointing to the generated file names.

<link rel="shortcut icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/js/favicon-32x32.png">

We make sure to keep only the links we need. This way, when the index.html file is processed by webpack, the appropriate loader will pick the assets up and adjust them into the final form we need. How do we know index.html will be processed by webpack? Because of html-webpack-plugin we introduced earlier.

Delete files #

Cleaning up earlier generated assets is always a good practice. Let's use it for the favicon-generator as well.

favicon-generator/utils/delete-files.ts #

Create favicon-generator/utils/delete-files.ts file

import del from 'del'
import { configuration } from '../config'

export const deleteFiles = async () => {
    const deletedFiles = await del([
        `${configuration.destination}/**/*`,
        `!${configuration.destination}`
    ], {
        force: true
    })

    console.log('Deleted files:')
    console.log(deletedFiles.join('\n'))
}

We just clean up all the files/folders in the destination folder leaving the folder itself intact.

Save data #

favicon-generator/utils/save-data.ts #

Create favicon-generator/utils/save-data.ts file:

import util from 'util'
import path from 'path'
import fs from 'fs'
import { Result } from './common'
import { configuration } from '../config'

const writeFile = util.promisify(fs.writeFile)
const readFile = util.promisify(fs.readFile)

export const save = async (data: Result) => {

    const { marker} = configuration.html
    const markerRegex = new RegExp(`${marker.start}[\\s\\S]*${marker.end}`)

    const actions = data.images.map(item =>
        writeFile(path.resolve(configuration.destination, item.name), item.contents)
    )
    .concat(
        data.files.map((item) =>
            writeFile(path.resolve(configuration.destination, item.name), item.contents))
    )
    .concat(
        readFile(configuration.html.source, 'utf8')
        .then((html) =>
            html.replace(
                markerRegex,
                `${marker.start}\n${data.html.join('\n')}\n${marker.end}`
            )
        )
        .then((html) =>
            writeFile(configuration.html.source, html)
        )
    )

    await Promise.all(actions)
    .catch((error: Error) => console.log(error.message))
}

With all the assets generated, we copy them into the appropriate places. Images and files (manifests) are moved into the destination folder. HTML links get inserted into the HTML source file (index.html).

Configuration #

Just as with the main application, we have to configure the TypeScript compilation step as well.

favicon-generator/tsconfig.json #

Create favicon-generator/tsconfig.json file

{
  "compilerOptions": {
    "target": "ESNEXT",                 /* Specify ECMAScript target version: 'ES3' (default) */"module": "commonjs",               /* Specify module code generation: 'none', 'commonjs', */ "allowJs": true,                    /* Allow javascript files to be compiled. */
    "noEmit": true,                     /* Do not emit outputs. */
    "strict": true,                     /* Enable all strict type-checking options. */
    "esModuleInterop": true             /* Enables emit interoperability between CommonJS and ES
  }
}

Alter favicon-generator/package.json with the appropriate script entries:

{
    "scripts": {
        "start": "cross-env TS_NODE_PROJECT=\"./tsconfig.json\" ts-node index.ts",
    }
}

Main application #

HTML #

webpack/index.html #

Create webpack/index.html file:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <!-- <favicon> -->
        <!-- </favicon> -->
    <title>Webpack App</title>
    </head>
    <body>
    </body>
 </html>

Configuration #

For the remaining assets (images, manifest), we need to copy them into the destination folder. This will be accomplished in two steps. First, for the dev environment, favicons are not critical, so to save time we will not copy favicon assets. Second, for production, we introduce a favicon copy step to make sure all the assets get into the dist folder. Because referencing favicons on the loaded website is the responsibility of the HTML page, we are moving HtmlWebpackPlugin from parts.ts to the dev and prod webpack configs separately. In dev mode, we have a plain index.htm (no favicons). In production, all the favicon files are referenced and copied to the dist folder.

webpack/parts.ts #

Change webpack/parts.ts file. Remove HtmlWebpackPlugin reference.

webpack/webpack.config.dev.ts #

Change webpack/webpack.config.dev.ts:

// ...
plugins: parts.plugins.concat([
    new HtmlWebpackPlugin({
        chunksSortMode: 'auto',
        filename: '../index.html',
        alwaysWriteToDisk: true,
        minify: false
    })
]),
// ...

webpack/webpack.config.ts #

Change webpack/webpack.config.ts file.

import CopyPlugin from 'copy-webpack-plugin'

// ...

plugins: [
    ...parts.plugins.concat([
            new HtmlWebpackPlugin({
            chunksSortMode: 'auto',
            filename: '../index.html',
            template: '../webpack/index.html',
            alwaysWriteToDisk: true,
            minify: false
        }),
    ]),

    new webpack.SourceMapDevToolPlugin({
        filename: '../js/[name].js.map',
    }),

    new MiniCssExtractPlugin({
        filename: '../css/[name].css',
        chunkFilename: '../css/[name].css',
    }),
    new CopyPlugin([{
        from: '../assets/favicons',
        to: folders.dist()
    },
    {
        from: '../assets/favicons/manifest.json',
        to:folders.dist()
    }])
],
// ...

package.json #

Alter package.json file to include favicon-generator run script:

"favicons": "cd ./favicon-generator/ && npm start"

Final version #

Reference implementation (opens in a new tab)