diff --git a/packages/gatsby-plugin-manifest/README.md b/packages/gatsby-plugin-manifest/README.md
index cf4e862e981d4..9fc2d621dba26 100644
--- a/packages/gatsby-plugin-manifest/README.md
+++ b/packages/gatsby-plugin-manifest/README.md
@@ -238,7 +238,9 @@ module.exports = {
#### Disable favicon
-Excludes `` link tag to html output. You can set `include_favicon` plugin option to `false` to opt-out of this behavior.
+A favicon is generated by default in automatic and hybrid modes (a 32x32 PNG, included via a `` tag in the document head).
+
+You can set the `include_favicon` plugin option to `false` to opt-out of this behavior.
```js
// in gatsby-config.js
diff --git a/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-node.js.snap b/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-node.js.snap
index 1580eba43d9f7..ca2789e272e64 100644
--- a/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-node.js.snap
+++ b/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-node.js.snap
@@ -9,7 +9,7 @@ exports[`Test plugin manifest options does file name based cache busting 1`] = `
"calls": Array [
Array [
"public/manifest.webmanifest",
- "{\\"name\\":\\"GatsbyJS\\",\\"short_name\\":\\"GatsbyJS\\",\\"start_url\\":\\"/\\",\\"background_color\\":\\"#f7f0eb\\",\\"theme_color\\":\\"#a2466c\\",\\"display\\":\\"standalone\\",\\"icons\\":[{\\"src\\":\\"icons/icon-48x48-contentDigest.png\\",\\"sizes\\":\\"48x48\\",\\"type\\":\\"image/png\\",\\"purpose\\":\\"all\\"},{\\"src\\":\\"icons/icon-128x128-contentDigest.png\\",\\"sizes\\":\\"128x128\\",\\"type\\":\\"image/png\\"}]}",
+ "{\\"name\\":\\"GatsbyJS\\",\\"short_name\\":\\"GatsbyJS\\",\\"start_url\\":\\"/\\",\\"background_color\\":\\"#f7f0eb\\",\\"theme_color\\":\\"#a2466c\\",\\"display\\":\\"standalone\\",\\"icons\\":[{\\"src\\":\\"icons/icon-48x48-00913339321ee5a854812aea11f8a5d4.png\\",\\"sizes\\":\\"48x48\\",\\"type\\":\\"image/png\\",\\"purpose\\":\\"all\\"},{\\"src\\":\\"icons/icon-128x128-00913339321ee5a854812aea11f8a5d4.png\\",\\"sizes\\":\\"128x128\\",\\"type\\":\\"image/png\\"}]}",
],
],
"results": Array [
diff --git a/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-ssr.js.snap b/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-ssr.js.snap
index 9b9d68e552d14..f7b95fedf987e 100644
--- a/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-ssr.js.snap
+++ b/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-ssr.js.snap
@@ -32,7 +32,7 @@ Array [
exports[`gatsby-plugin-manifest Cache Busting Does file name cache busting if "cache_busting_mode" option is set to name 1`] = `
Array [
,
,
,
,
,
,
,
,
,
,
@@ -85,7 +85,7 @@ Array [
exports[`gatsby-plugin-manifest Cache Busting Does query cache busting if "cache_busting_mode" option is set to query 1`] = `
Array [
,
,
,
,
,
,
,
,
,
,
@@ -138,7 +138,7 @@ Array [
exports[`gatsby-plugin-manifest Cache Busting Does query cache busting if "cache_busting_mode" option is set to undefined 1`] = `
Array [
,
,
,
,
,
,
,
,
,
,
@@ -191,7 +191,7 @@ Array [
exports[`gatsby-plugin-manifest Cache Busting doesn't add cache busting if "cache_busting_mode" option is set to none 1`] = `
Array [
,
,
,
,
,
,
,
,
,
,
,
,
diff --git a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js
index 48cca31da2f4d..1c750c871197e 100644
--- a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js
+++ b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js
@@ -43,7 +43,7 @@ jest.mock(`gatsby-core-utils`, () => {
return {
slash: originalCoreUtils.slash,
cpuCoreCount: jest.fn(() => `1`),
- createContentDigest: jest.fn(() => `contentDigest`),
+ createContentDigest: originalCoreUtils.createContentDigest,
}
})
@@ -86,6 +86,13 @@ const manifestOptions = {
],
}
+// Some of these tests check the number of sharp calls to assert that the
+// correct number of images are created.
+//
+// As long as an `icon` is defined in the config, there's always an extra
+// call to sharp to check the source icon is square. Therefore the assertions
+// check for N + 1 sharp calls, where N is the expected number of icons
+// generated.
describe(`Test plugin manifest options`, () => {
beforeEach(() => {
fs.writeFileSync.mockReset()
@@ -151,6 +158,9 @@ describe(`Test plugin manifest options`, () => {
path.dirname(`other-icons/icon-48x48.png`)
)
+ // No sharp calls because this is manual mode: user provides all icon sizes
+ // rather than the plugin generating them
+ expect(sharp).toHaveBeenCalledTimes(0)
expect(fs.mkdirSync).toHaveBeenNthCalledWith(1, firstIconPath)
expect(fs.mkdirSync).toHaveBeenNthCalledWith(2, secondIconPath)
})
@@ -177,8 +187,44 @@ describe(`Test plugin manifest options`, () => {
...pluginSpecificOptions,
})
+ // One call to sharp to check the source icon is square
+ // + another for the favicon (enabled by default)
+ // + another for the single icon in the `icons` config
+ // => 3 total calls
+ expect(sharp).toHaveBeenCalledTimes(3)
+ expect(sharp).toHaveBeenCalledWith(icon, { density: 32 }) // the default favicon
expect(sharp).toHaveBeenCalledWith(icon, { density: size })
+ })
+
+ it(`skips favicon generation if "include_favicon" option is set to false`, async () => {
+ fs.statSync.mockReturnValueOnce({ isFile: () => true })
+
+ const icon = `pretend/this/exists.png`
+ const size = 48
+
+ const pluginSpecificOptions = {
+ icon: icon,
+ icons: [
+ {
+ src: `icons/icon-48x48.png`,
+ sizes: `${size}x${size}`,
+ type: `image/png`,
+ },
+ ],
+ include_favicon: false,
+ }
+
+ await onPostBootstrap(apiArgs, {
+ ...manifestOptions,
+ ...pluginSpecificOptions,
+ })
+
+ // Only two sharp calls here: one to check the source icon size,
+ // and another to generate the single icon in the config.
+ // By default, there would be a 3rd call for the favicon, but that's
+ // disabled by the `include_favicon` option.
expect(sharp).toHaveBeenCalledTimes(2)
+ expect(sharp).toHaveBeenCalledWith(icon, { density: size })
})
it(`fails on non existing icon`, async () => {
@@ -236,7 +282,11 @@ describe(`Test plugin manifest options`, () => {
...pluginSpecificOptions,
})
- expect(sharp).toHaveBeenCalledTimes(2)
+ // Two icons in the config, plus a favicon, plus one call to check the
+ // source icon size => 4 total calls to sharp.
+ expect(sharp).toHaveBeenCalledTimes(4)
+
+ // Filenames in the manifest should be suffixed with the content digest
expect(fs.writeFileSync).toMatchSnapshot()
})
@@ -253,7 +303,9 @@ describe(`Test plugin manifest options`, () => {
...pluginSpecificOptions,
})
- expect(sharp).toHaveBeenCalledTimes(2)
+ // Two icons in the config, plus a favicon, plus one call to check the
+ // source icon size => 4 total calls to sharp.
+ expect(sharp).toHaveBeenCalledTimes(4)
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.anything(),
JSON.stringify(manifestOptions)
@@ -274,7 +326,9 @@ describe(`Test plugin manifest options`, () => {
...pluginSpecificOptions,
})
- expect(sharp).toHaveBeenCalledTimes(2)
+ // Two icons in the config, plus a favicon, plus one call to check the
+ // source icon size => 4 total calls to sharp.
+ expect(sharp).toHaveBeenCalledTimes(4)
const content = JSON.parse(fs.writeFileSync.mock.calls[0][1])
expect(content.icons[0].purpose).toEqual(`all`)
expect(content.icons[1].purpose).toEqual(`maskable`)
@@ -338,6 +392,7 @@ describe(`Test plugin manifest options`, () => {
JSON.stringify(expectedResults[2])
)
})
+
it(`generates all language versions with pathPrefix`, async () => {
fs.statSync.mockReturnValueOnce({ isFile: () => true })
const pluginSpecificOptions = {
diff --git a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js
index 7eb4142a00569..24d3e32bbc70f 100644
--- a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js
+++ b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js
@@ -4,12 +4,6 @@ jest.mock(`fs`, () => {
}
})
-jest.mock(`gatsby-core-utils`, () => {
- return {
- createContentDigest: jest.fn(() => `contentDigest`),
- }
-})
-
const { onRenderBody } = require(`../gatsby-ssr`)
let headComponents
diff --git a/packages/gatsby-plugin-manifest/src/common.js b/packages/gatsby-plugin-manifest/src/common.js
index 8b05a0700468c..df5e5e692057a 100644
--- a/packages/gatsby-plugin-manifest/src/common.js
+++ b/packages/gatsby-plugin-manifest/src/common.js
@@ -1,6 +1,14 @@
import fs from "fs"
import sysPath from "path"
+exports.favicons = [
+ {
+ src: `favicon-32x32.png`,
+ sizes: `32x32`,
+ type: `image/png`,
+ },
+]
+
// default icons for generating icons
exports.defaultIcons = [
{
diff --git a/packages/gatsby-plugin-manifest/src/gatsby-node.js b/packages/gatsby-plugin-manifest/src/gatsby-node.js
index 30cc7072e07d9..db7f45d94c6bf 100644
--- a/packages/gatsby-plugin-manifest/src/gatsby-node.js
+++ b/packages/gatsby-plugin-manifest/src/gatsby-node.js
@@ -2,7 +2,12 @@ import fs from "fs"
import path from "path"
import sharp from "./safe-sharp"
import { createContentDigest, cpuCoreCount, slash } from "gatsby-core-utils"
-import { defaultIcons, doesIconExist, addDigestToPath } from "./common"
+import {
+ defaultIcons,
+ doesIconExist,
+ addDigestToPath,
+ favicons,
+} from "./common"
sharp.simd(true)
@@ -126,6 +131,8 @@ const makeManifest = async ({
const suffix =
shouldLocalize && pluginOptions.lang ? `_${pluginOptions.lang}` : ``
+ const faviconIsEnabled = pluginOptions.include_favicon ?? true
+
// Delete options we won't pass to the manifest.webmanifest.
delete manifest.plugins
delete manifest.legacy
@@ -192,30 +199,46 @@ const makeManifest = async ({
const iconDigest = createContentDigest(fs.readFileSync(icon))
- //if cacheBusting is being done via url query icons must be generated before cache busting runs
- if (cacheMode === `query`) {
- await Promise.all(
- manifest.icons.map(dstIcon =>
- checkCache(cache, dstIcon, icon, iconDigest, generateIcon)
+ /**
+ * Given an array of icon configs, generate the various output sizes from
+ * the source icon image.
+ */
+ async function processIconSet(iconSet) {
+ //if cacheBusting is being done via url query icons must be generated before cache busting runs
+ if (cacheMode === `query`) {
+ await Promise.all(
+ iconSet.map(dstIcon =>
+ checkCache(cache, dstIcon, icon, iconDigest, generateIcon)
+ )
)
- )
- }
+ }
- if (cacheMode !== `none`) {
- manifest.icons = manifest.icons.map(icon => {
- let newIcon = { ...icon }
- newIcon.src = addDigestToPath(icon.src, iconDigest, cacheMode)
- return newIcon
- })
- }
+ if (cacheMode !== `none`) {
+ iconSet = iconSet.map(icon => {
+ let newIcon = { ...icon }
+ newIcon.src = addDigestToPath(icon.src, iconDigest, cacheMode)
+ return newIcon
+ })
+ }
- //if file names are being modified by cacheBusting icons must be generated after cache busting runs
- if (cacheMode !== `query`) {
- await Promise.all(
- manifest.icons.map(dstIcon =>
- checkCache(cache, dstIcon, icon, iconDigest, generateIcon)
+ //if file names are being modified by cacheBusting icons must be generated after cache busting runs
+ if (cacheMode !== `query`) {
+ await Promise.all(
+ iconSet.map(dstIcon =>
+ checkCache(cache, dstIcon, icon, iconDigest, generateIcon)
+ )
)
- )
+ }
+
+ return iconSet
+ }
+
+ manifest.icons = await processIconSet(manifest.icons)
+
+ // If favicon is enabled, apply the same caching policy and generate
+ // the resized image(s)
+ if (faviconIsEnabled) {
+ await processIconSet(favicons)
}
}
diff --git a/packages/gatsby-plugin-manifest/src/gatsby-ssr.js b/packages/gatsby-plugin-manifest/src/gatsby-ssr.js
index 17f5a15321a4e..ea655902c4422 100644
--- a/packages/gatsby-plugin-manifest/src/gatsby-ssr.js
+++ b/packages/gatsby-plugin-manifest/src/gatsby-ssr.js
@@ -2,7 +2,7 @@ import React from "react"
import { withPrefix as fallbackWithPrefix, withAssetPrefix } from "gatsby"
import fs from "fs"
import { createContentDigest } from "gatsby-core-utils"
-import { defaultIcons, addDigestToPath } from "./common.js"
+import { defaultIcons, addDigestToPath, favicons } from "./common.js"
import getManifestForPathname from "./get-manifest-pathname"
// TODO: remove for v3
@@ -30,8 +30,6 @@ exports.onRenderBody = (
// If icons were generated, also add a favicon link.
if (srcIconExists) {
- const favicon = icons && icons.length ? icons[0].src : null
-
if (cacheBusting !== `none`) {
iconDigest = createContentDigest(fs.readFileSync(pluginOptions.icon))
}
@@ -41,14 +39,18 @@ exports.onRenderBody = (
? pluginOptions.include_favicon
: true
- if (favicon && insertFaviconLinkTag) {
- headComponents.push(
-
- )
+ if (insertFaviconLinkTag) {
+ favicons.forEach(favicon => {
+ headComponents.push(
+
+ )
+ })
}
}