Skip to main content

Favicon

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

As far as webpack is concerned, all favicon formats could be split in two groups:

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

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

There are special webpack plugins for generating a set of favicons during a build time. They all provide options to optimise their work (minimizing build time) by various mechanisms (ex. caching). But is it really necessary to generate all the favicons on every build? We are going to be looking into slightly different approach that generates favicons on demand (webpack build independent) and saves them into the source folder. Later, any particular webpack loader of the choice could pick them up and move to the destination folder. This approach requires more implementation, but it also exposes some new ways of dealing with assets.

Dependencies

Following the idea described above, we are going to be using as minimum dependencies as possible. In particular:

  • favicons - generates favicon assets based on a config
  • copy-webpack-plugin - webpack plugin that copes files from the source to destination via 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 favicons package dependency in our main app (overwise it will be restored in every CI build). For that reason we can create another NodeJS based project just for favicons generation. We are going to create that project as a suborder inside our root repository folder.

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 the 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 major bulk of the work will be concentrated inside favicon-generator folder. First, we need to understand how favicons package works. Given a config file in a form of a JavaScript object and a path to the source file assets to be generated from, 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 image files generated and the manifest file. What file generator needs to do is to take all of that information and put it into the corresponding places within the project structure so webpack can pick them up and transfer to the destination folder.

The file generator work will consist of free main parts: generate data, clean up previously generated information (if any), 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 favicons package into the information that could be plugged into the webpack pipeline. There is nothing we need to do with images and files. Those assets will be copied into the appropriate places of the source tree. But the HTML is a different story. 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 are making sure that we filter out only links we need. This way, when index.html file is processed by the webpack, 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 the 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 are copying them into appropriate places. Images and files (manifests) are moved into the destination folder. HTML links get substituted into the HTML source file (index.html).

Configuration

Just as with the main application, we have to configure 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 rest of the 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 that critical. So to save compilation time, we are not going to copy favicon assets in this case. Second, for production, we are introducing copy favicons step, to make sure all the assets get into the dist folder. Because referencing favicons in in the loaded website is a responsibility of the HTM page, we are moving HtmlWebpackPlugin from the parts.ts to dev and prod webpack configs separately. In dev mode, we have 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