Skip to content

Commit

Permalink
feat(plugin-manifest): support SVG favicon (#25276)
Browse files Browse the repository at this point in the history
Co-authored-by: Yogi <me@yogi.codes>
  • Loading branch information
moonmeister and Yogi committed Jul 23, 2020
1 parent 08d2d70 commit f99ae04
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 5 deletions.
2 changes: 1 addition & 1 deletion packages/gatsby-plugin-manifest/README.md
Expand Up @@ -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 `<link rel="icon" />` tag in the document head).
A favicon is generated by default in automatic and hybrid modes (a 32x32 PNG, included via a `<link rel="icon" />` 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.

Expand Down
Expand Up @@ -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 [
<link
href="/favicon-00913339321ee5a854812aea11f8a5d4.svg"
rel="icon"
type="image/svg+xml"
/>,
<link
href="/favicon-32x32-00913339321ee5a854812aea11f8a5d4.png"
rel="icon"
type="image/png"
/>,
<link
href="/manifest.webmanifest"
Expand Down Expand Up @@ -84,9 +90,15 @@ Array [

exports[`gatsby-plugin-manifest Cache Busting Does query cache busting if "cache_busting_mode" option is set to query 1`] = `
Array [
<link
href="/favicon.svg?v=00913339321ee5a854812aea11f8a5d4"
rel="icon"
type="image/svg+xml"
/>,
<link
href="/favicon-32x32.png?v=00913339321ee5a854812aea11f8a5d4"
rel="icon"
type="image/png"
/>,
<link
href="/manifest.webmanifest"
Expand Down Expand Up @@ -137,9 +149,15 @@ Array [

exports[`gatsby-plugin-manifest Cache Busting Does query cache busting if "cache_busting_mode" option is set to undefined 1`] = `
Array [
<link
href="/favicon.svg?v=00913339321ee5a854812aea11f8a5d4"
rel="icon"
type="image/svg+xml"
/>,
<link
href="/favicon-32x32.png?v=00913339321ee5a854812aea11f8a5d4"
rel="icon"
type="image/png"
/>,
<link
href="/manifest.webmanifest"
Expand Down Expand Up @@ -190,9 +208,15 @@ Array [

exports[`gatsby-plugin-manifest Cache Busting doesn't add cache busting if "cache_busting_mode" option is set to none 1`] = `
Array [
<link
href="/favicon.svg"
rel="icon"
type="image/svg+xml"
/>,
<link
href="/favicon-32x32.png"
rel="icon"
type="image/png"
/>,
<link
href="/manifest.webmanifest"
Expand Down Expand Up @@ -262,9 +286,15 @@ Array [

exports[`gatsby-plugin-manifest Favicon Adds link favicon tag if "include_favicon" is set to true 1`] = `
Array [
<link
href="/favicon.svg"
rel="icon"
type="image/svg+xml"
/>,
<link
href="/favicon-32x32.png"
rel="icon"
type="image/png"
/>,
<link
href="/manifest.webmanifest"
Expand Down Expand Up @@ -424,9 +454,15 @@ Array [

exports[`gatsby-plugin-manifest Manifest Link Generation Adds "icon" and "manifest" links and "theme_color" meta tag to head 1`] = `
Array [
<link
href="/favicon.svg?v=00913339321ee5a854812aea11f8a5d4"
rel="icon"
type="image/svg+xml"
/>,
<link
href="/favicon-32x32.png?v=00913339321ee5a854812aea11f8a5d4"
rel="icon"
type="image/png"
/>,
<link
href="/manifest.webmanifest"
Expand Down
37 changes: 37 additions & 0 deletions packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js
Expand Up @@ -4,6 +4,7 @@ jest.mock(`fs`, () => {
writeFileSync: jest.fn(),
mkdirSync: jest.fn(),
readFileSync: jest.fn().mockImplementation(() => `someIconImage`),
copyFileSync: jest.fn(),
statSync: jest.fn(),
}
})
Expand All @@ -27,6 +28,7 @@ jest.mock(`sharp`, () => {
return {
width: 128,
height: 128,
format: `png`,
}
}
})()
Expand Down Expand Up @@ -98,6 +100,7 @@ describe(`Test plugin manifest options`, () => {
fs.writeFileSync.mockReset()
fs.mkdirSync.mockReset()
fs.existsSync.mockReset()
fs.copyFileSync.mockReset()
sharp.mockClear()
})

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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)
})
})
7 changes: 4 additions & 3 deletions packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js
Expand Up @@ -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: `/`,
Expand Down Expand Up @@ -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()
})
})
Expand All @@ -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`,
})
Expand Down
4 changes: 4 additions & 0 deletions packages/gatsby-plugin-manifest/src/gatsby-node.js
Expand Up @@ -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`))
}
}
}

Expand Down
15 changes: 14 additions & 1 deletion packages/gatsby-plugin-manifest/src/gatsby-ssr.js
Expand Up @@ -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(
<link
key={`gatsby-plugin-manifest-icon-link-svg`}
rel="icon"
href={withPrefix(
addDigestToPath(`favicon.svg`, cacheDigest, cacheBusting)
)}
type="image/svg+xml"
/>
)
}
favicons.forEach(favicon => {
headComponents.push(
<link
key={`gatsby-plugin-manifest-icon-link`}
key={`gatsby-plugin-manifest-icon-link-png`}
rel="icon"
href={withPrefix(
addDigestToPath(favicon.src, cacheDigest, cacheBusting)
)}
type="image/png"
/>
)
})
Expand Down

0 comments on commit f99ae04

Please sign in to comment.