diff --git a/packages/gatsby-plugin-manifest/README.md b/packages/gatsby-plugin-manifest/README.md
index a697c04a6e4ff..cd3c725673bcf 100644
--- a/packages/gatsby-plugin-manifest/README.md
+++ b/packages/gatsby-plugin-manifest/README.md
@@ -238,7 +238,7 @@ module.exports = {
#### Disable favicon
-A favicon is generated by default in automatic and hybrid modes (a 32x32 PNG, included via a `` tag in the document head).
+A favicon is generated by default in automatic and hybrid modes (a 32x32 PNG, included via a `` tag in the document head). Additionally, if an SVG icon is provided as the source, it will be used in the document head without modification as a favicon. The PNG will still be created and included as a fallback. Including the SVG icon allows creating a responsive icon with CSS Media Queries such as [dark mode](https://catalin.red/svg-favicon-light-dark-theme/#browser-support-and-fallbacks) and [others](https://css-tricks.com/svg-favicons-and-all-the-fun-things-we-can-do-with-them/#other-media-queries).
You can set the `include_favicon` plugin option to `false` to opt-out of this behavior.
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 f7b95fedf987e..52c5840f11c93 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
@@ -31,9 +31,15 @@ Array [
exports[`gatsby-plugin-manifest Cache Busting Does file name cache busting if "cache_busting_mode" option is set to name 1`] = `
Array [
+ ,
,
,
,
,
,
,
,
,
,
,
,
{
writeFileSync: jest.fn(),
mkdirSync: jest.fn(),
readFileSync: jest.fn().mockImplementation(() => `someIconImage`),
+ copyFileSync: jest.fn(),
statSync: jest.fn(),
}
})
@@ -27,6 +28,7 @@ jest.mock(`sharp`, () => {
return {
width: 128,
height: 128,
+ format: `png`,
}
}
})()
@@ -98,6 +100,7 @@ describe(`Test plugin manifest options`, () => {
fs.writeFileSync.mockReset()
fs.mkdirSync.mockReset()
fs.existsSync.mockReset()
+ fs.copyFileSync.mockReset()
sharp.mockClear()
})
@@ -225,6 +228,7 @@ describe(`Test plugin manifest options`, () => {
// disabled by the `include_favicon` option.
expect(sharp).toHaveBeenCalledTimes(2)
expect(sharp).toHaveBeenCalledWith(icon, { density: size })
+ expect(fs.copyFileSync).toHaveBeenCalledTimes(0)
})
it(`fails on non existing icon`, async () => {
@@ -485,4 +489,37 @@ describe(`Test plugin manifest options`, () => {
JSON.stringify(expectedResults[2])
)
})
+
+ it(`writes SVG to public if src icon is SVG`, async () => {
+ sharp.mockReturnValueOnce({
+ metadata: () => {
+ return { format: `svg` }
+ },
+ })
+ const icon = `this/is/an/icon.svg`
+ const specificOptions = {
+ ...manifestOptions,
+ icon: icon,
+ }
+
+ await onPostBootstrap({ ...apiArgs }, specificOptions)
+
+ expect(fs.copyFileSync).toHaveBeenCalledWith(
+ expect.stringContaining(`icon.svg`),
+ expect.stringContaining(`favicon.svg`)
+ )
+
+ expect(fs.copyFileSync).toHaveBeenCalledTimes(1)
+ })
+
+ it(`does not write SVG to public if src icon is PNG`, async () => {
+ const specificOptions = {
+ ...manifestOptions,
+ icon: `this/is/an/icon.png`,
+ }
+
+ await onPostBootstrap({ ...apiArgs }, specificOptions)
+
+ expect(fs.copyFileSync).toHaveBeenCalledTimes(0)
+ })
})
diff --git a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js
index 159670b965e53..61bbabf6bd763 100644
--- a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js
+++ b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js
@@ -15,7 +15,8 @@ const onRenderBody = (args, pluginOptions) => {
let headComponents
const setHeadComponents = args => (headComponents = headComponents.concat(args))
-const defaultIcon = `pretend/this/exists.png`
+const defaultIcon = `pretend/this/exists.svg`
+
const ssrArgs = {
setHeadComponents,
pathname: `/`,
@@ -276,7 +277,7 @@ describe(`gatsby-plugin-manifest`, () => {
})
it(`Does query cache busting if "cache_busting_mode" option is set to undefined`, () => {
- onRenderBody(ssrArgs, { icon: true })
+ onRenderBody(ssrArgs, { icon: defaultIcon })
expect(headComponents).toMatchSnapshot()
})
})
@@ -285,7 +286,7 @@ describe(`gatsby-plugin-manifest`, () => {
it(`Adds link favicon tag if "include_favicon" is set to true`, () => {
onRenderBody(ssrArgs, {
icon: defaultIcon,
- include_favicon: defaultIcon,
+ include_favicon: true,
legacy: false,
cache_busting_mode: `none`,
})
diff --git a/packages/gatsby-plugin-manifest/src/gatsby-node.js b/packages/gatsby-plugin-manifest/src/gatsby-node.js
index 508e0d10fb3f5..f1504230109d9 100644
--- a/packages/gatsby-plugin-manifest/src/gatsby-node.js
+++ b/packages/gatsby-plugin-manifest/src/gatsby-node.js
@@ -253,6 +253,10 @@ const makeManifest = async ({
// the resized image(s)
if (faviconIsEnabled) {
await processIconSet(favicons)
+
+ if (metadata.format === `svg`) {
+ fs.copyFileSync(icon, path.join(`public`, `favicon.svg`))
+ }
}
}
diff --git a/packages/gatsby-plugin-manifest/src/gatsby-ssr.js b/packages/gatsby-plugin-manifest/src/gatsby-ssr.js
index 223bd199e3318..b35a5d0859bad 100644
--- a/packages/gatsby-plugin-manifest/src/gatsby-ssr.js
+++ b/packages/gatsby-plugin-manifest/src/gatsby-ssr.js
@@ -31,14 +31,27 @@ exports.onRenderBody = (
// If icons were generated, also add a favicon link.
if (srcIconExists) {
if (insertFaviconLinkTag) {
+ if (icon?.endsWith(`.svg`)) {
+ headComponents.push(
+
+ )
+ }
favicons.forEach(favicon => {
headComponents.push(
)
})