diff --git a/gatsby-config.js b/gatsby-config.js index 33b5c3a..3506f96 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -260,20 +260,39 @@ module.exports = { resolve: `gatsby-plugin-canonical-urls`, options: { siteUrl: homepage }, }, + // { + // resolve: `@gaiama/gatsby-plugin-manifest`, + // options: { + // manifests: [ + // Object.assign({}, sharedManifestProperties, { + // start_url: `/en/?ref=a2hs`, + // // pattern: `^/en/.*$`, + // language: `en`, + // }), + // Object.assign({}, sharedManifestProperties, { + // start_url: `/de/?ref=a2hs`, + // // pattern: `^/de/.*$`, + // language: `de`, + // }), + // ], + // }, + // }, { resolve: `@gaiama/gatsby-plugin-manifest`, options: { manifests: [ - Object.assign({}, sharedManifestProperties, { - start_url: `/en/?ref=a2hs`, - // pattern: `^/en/.*$`, - language: `en`, - }), - Object.assign({}, sharedManifestProperties, { + { + ...sharedManifestProperties, start_url: `/de/?ref=a2hs`, - // pattern: `^/de/.*$`, + regex: `^/de/.*`, language: `de`, - }), + }, + { + ...sharedManifestProperties, + start_url: `/en/?ref=a2hs`, + regex: `^/en/.*`, + language: `en`, + }, ], }, }, diff --git a/package.json b/package.json index 7828c87..48c6dc4 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@fortawesome/free-regular-svg-icons": "5.8.1", "@fortawesome/free-solid-svg-icons": "5.8.1", "@fortawesome/react-fontawesome": "0.1.4", - "@gaiama/gatsby-plugin-manifest": "2.0.5", + "@gaiama/gatsby-plugin-manifest": "2.0.29", "@gaiama/query-string": "0.1.0", "@gaiama/react-video-player": "0.3.0", "@mdx-js/mdx": "^0.20.3", diff --git a/packages/gatsby-plugin-manifest/.babelrc b/packages/gatsby-plugin-manifest/.babelrc new file mode 100644 index 0000000..aaa3607 --- /dev/null +++ b/packages/gatsby-plugin-manifest/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": [ + ["babel-preset-gatsby-package", { "browser": true }] + ] +} + diff --git a/packages/gatsby-plugin-manifest/.npmignore b/packages/gatsby-plugin-manifest/.npmignore new file mode 100644 index 0000000..e771d2c --- /dev/null +++ b/packages/gatsby-plugin-manifest/.npmignore @@ -0,0 +1,34 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules +*.un~ +yarn.lock +src +flow-typed +coverage +decls +examples diff --git a/packages/gatsby-plugin-manifest/CHANGELOG.md b/packages/gatsby-plugin-manifest/CHANGELOG.md new file mode 100644 index 0000000..31b068c --- /dev/null +++ b/packages/gatsby-plugin-manifest/CHANGELOG.md @@ -0,0 +1,234 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.0.29](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.28...gatsby-plugin-manifest@2.0.29) (2019-04-10) + +### Bug Fixes + +- **gatsby-plugin-manifest:** fix regression with sharp failing to load ([#13271](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/13271)) ([c264a85](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/c264a85)), closes [#13055](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/13264) + +## [2.0.28](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.27...gatsby-plugin-manifest@2.0.28) (2019-04-09) + +### Bug Fixes + +- **gatsby-plugin-manifest:** allow multiple icon paths ([#13059](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/13059)) ([5dcde0d](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/5dcde0d)), closes [#13055](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/13055) + +## [2.0.27](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.26...gatsby-plugin-manifest@2.0.27) (2019-04-08) + +**Note:** Version bump only for package gatsby-plugin-manifest + +## [2.0.26](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.25...gatsby-plugin-manifest@2.0.26) (2019-03-29) + +### Bug Fixes + +- **gatsby-plugin-manifest:** ensure icon_options is stripped ([#12907](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/12907)) ([201a4f5](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/201a4f5)) + +## [2.0.25](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.24...gatsby-plugin-manifest@2.0.25) (2019-03-27) + +### Bug Fixes + +- **gatsby-plugin-manifest:** Fix incorrect favicons size bug ([#12081](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/12081)) ([366980b](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/366980b)), closes [#12051](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/12051) [#12051](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/12051) + +### Features + +- **gatsby-plugin-manifest:** add icon_options as an option to support the purpose property ([#12794](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/12794)) ([127f232](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/127f232)), closes [#12793](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/12793) + +## [2.0.24](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.23...gatsby-plugin-manifest@2.0.24) (2019-03-12) + +### Features + +- **gatsby-plugin-manifest:** add cache busting to icon url ([#8343](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/8343)) ([5f656f8](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/5f656f8)) + +## [2.0.23](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.22...gatsby-plugin-manifest@2.0.23) (2019-03-11) + +**Note:** Version bump only for package gatsby-plugin-manifest + +## [2.0.22](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.21...gatsby-plugin-manifest@2.0.22) (2019-03-05) + +### Bug Fixes + +- don't crash if cpu-core-count is not available ([#12332](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/12332)) ([412217d](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/412217d)) + +## [2.0.21](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.20...gatsby-plugin-manifest@2.0.21) (2019-03-04) + +### Features + +- **gatsby:** configure physical cores, logical_cores or fixed number ([#10257](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/10257)) ([c51440e](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/c51440e)) + +## [2.0.20](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.19...gatsby-plugin-manifest@2.0.20) (2019-02-28) + +**Note:** Version bump only for package gatsby-plugin-manifest + +## [2.0.19](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.18...gatsby-plugin-manifest@2.0.19) (2019-02-22) + +### Features + +- **gatsby-plugin-manifest:** add option for crossorigin in manifest ([#11953](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/11953)) ([1a16600](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/1a16600)) + +## [2.0.18](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.17...gatsby-plugin-manifest@2.0.18) (2019-02-19) + +### Bug Fixes + +- **gatsby-plugin-manifest:** improve SVG->PNG fidelity ([#11608](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/11608)) ([e9345cd](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/e9345cd)) + +## [2.0.17](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.16...gatsby-plugin-manifest@2.0.17) (2019-02-01) + +**Note:** Version bump only for package gatsby-plugin-manifest + +## [2.0.16](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.15...gatsby-plugin-manifest@2.0.16) (2019-01-31) + +### Features + +- **gatsby-plugin-manifest:** make favicon link tag optional ([#11414](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/11414)) ([1af42bc](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/1af42bc)) + +## [2.0.15](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.14...gatsby-plugin-manifest@2.0.15) (2019-01-29) + +**Note:** Version bump only for package gatsby-plugin-manifest + +## [2.0.14](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.13...gatsby-plugin-manifest@2.0.14) (2019-01-28) + +### Bug Fixes + +- **gatsby-plugin-manifest:** Legacy default to true ([#11203](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/11203)) ([7e84613](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/7e84613)) + + + +## [2.0.13](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.12...gatsby-plugin-manifest@2.0.13) (2018-12-29) + +### Features + +- **gatsby-plugin-manifest:** add option to remove the "theme color" meta tag ([#10440](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/10440)) ([129c5d8](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/129c5d8)) + + + +## [2.0.12](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.11...gatsby-plugin-manifest@2.0.12) (2018-12-11) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.11](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.10...gatsby-plugin-manifest@2.0.11) (2018-11-29) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.10](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.9...gatsby-plugin-manifest@2.0.10) (2018-11-21) + +### Features + +- **gatsby-plugin-manifest:** don't output `theme-color` meta tag if it's not defiened ([#10069](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/10069)) ([7802470](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/7802470)) + + + +## [2.0.9](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.8...gatsby-plugin-manifest@2.0.9) (2018-11-12) + +### Features + +- **gatsby-plugin-manifest:** add option to generate apple-touch-icons links ([#7256](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/7256)) ([0b9d656](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/0b9d656)), closes [#5887](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/5887) + + + +## [2.0.8](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.7...gatsby-plugin-manifest@2.0.8) (2018-11-08) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.7](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.6...gatsby-plugin-manifest@2.0.7) (2018-10-29) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.6](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.5...gatsby-plugin-manifest@2.0.6) (2018-10-24) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.5](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.4...gatsby-plugin-manifest@2.0.5) (2018-10-09) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.4](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.3...gatsby-plugin-manifest@2.0.4) (2018-09-26) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.3](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.2-rc.1...gatsby-plugin-manifest@2.0.3) (2018-09-24) + +### Bug Fixes + +- **gatsby-plugin-manifest:** favicon path respects hybrid mode ([#8315](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/issues/8315)) ([05e7ccd](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/commit/05e7ccd)) + + + +## [2.0.2](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.2-rc.1...gatsby-plugin-manifest@2.0.2) (2018-09-17) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.2-rc.1](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.2-rc.0...gatsby-plugin-manifest@2.0.2-rc.1) (2018-08-29) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.2-rc.0](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.2-beta.7...gatsby-plugin-manifest@2.0.2-rc.0) (2018-08-21) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.2-beta.7](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.2-beta.6...gatsby-plugin-manifest@2.0.2-beta.7) (2018-08-20) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.2-beta.6](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.2-beta.5...gatsby-plugin-manifest@2.0.2-beta.6) (2018-08-15) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.2-beta.5](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.2-beta.4...gatsby-plugin-manifest@2.0.2-beta.5) (2018-08-10) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.2-beta.4](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.2-beta.3...gatsby-plugin-manifest@2.0.2-beta.4) (2018-08-07) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.2-beta.3](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.2-beta.2...gatsby-plugin-manifest@2.0.2-beta.3) (2018-07-21) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.2-beta.2](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.2-beta.1...gatsby-plugin-manifest@2.0.2-beta.2) (2018-06-20) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.2-beta.1](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@2.0.2-beta.0...gatsby-plugin-manifest@2.0.2-beta.1) (2018-06-17) + +**Note:** Version bump only for package gatsby-plugin-manifest + + + +## [2.0.2-beta.0](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest/compare/gatsby-plugin-manifest@1.0.27...gatsby-plugin-manifest@2.0.2-beta.0) (2018-06-17) + +**Note:** Version bump only for package gatsby-plugin-manifest diff --git a/packages/gatsby-plugin-manifest/README.md b/packages/gatsby-plugin-manifest/README.md new file mode 100644 index 0000000..141e317 --- /dev/null +++ b/packages/gatsby-plugin-manifest/README.md @@ -0,0 +1,447 @@ +# gatsby-plugin-manifest + +The web app manifest(part of the [PWA](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) specification) enabled by this plugin allows users to add your site to their home screen on most mobile browsers — +[see here](http://caniuse.com/#feat=web-app-manifest). The manifest provides configuration and icons to the phone. + +This plugin provides several features beyond manifest configuration to make your life easier, they are: + +- Auto icon generation - generates multiple icon sizes from a single source +- [Favicon support](https://www.w3.org/2005/10/howto-favicon) +- Legacy icon support (iOS)[^1] +- [Cache busting](https://www.keycdn.com/support/what-is-cache-busting) + +Each of these features has extensive configuration available so you're always in control. + +## Install + +``` +$ npm install --save gatsby-plugin-manifest +``` + +## How to use + +### Add plugin and manifest settings - **Required** + +```js +// in gatsby-config.js +module.exports = { + plugins: [ + { + resolve: `gatsby-plugin-manifest`, + options: { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + start_url: `/`, + background_color: `#f7f0eb`, + theme_color: `#a2466c`, + display: `standalone`, + }, + }, + ], +} +``` + +If you're using this plugin together with [`gatsby-plugin-offline`](https://www.gatsbyjs.org/packages/gatsby-plugin-offline) (recommended), +this plugin should be listed _before_ the offline plugin so that it can cache +the created `manifest.webmanifest`. + +For more information on configuring your web app [see here](https://developers.google.com/web/fundamentals/web-app-manifest/). + +### Configure icons and their generations - **Required** + +There are three modes in which icon generation can function: automatic, hybrid, and manual(disabled). These modes can affect other configurations defaults. + +- Automatic - Generate a pre-configured set of icons from a single source icon. + + - Favicon - yes + - Legacy icon support - yes + - Cache busting - yes + +- Hybrid - Generate a manually configured set of icons from a single source icon. + + - Favicon - yes + - Legacy icon support - yes + - Cache busting - yes + +- Manual - Don't generate or pre-configure any icons. + + - Favicon - never + - Legacy icon support - yes + - Cache busting - never + +- i18n - Generate separate versions for multiple languages + + - Supports all 3 modes + +**_IMPORTANT:_** For best results, if you're providing an icon for generation it should be... + +- ...at least as big as the largest icon being generated (512x512 by default). +- ...square (if it's not, transparent bars will add to make it square). +- ...of one of the follow formats: JPEG, PNG, WebP, TIFF, GIF or SVG. + +#### Automatic mode configuration + +Add the following line to the plugin options + +```js + icon: `src/images/icon.png`, // This path is relative to the root of the site. +``` + +Automatic mode is the easiest option for most people. + +#### Hybrid mode configuration + +Add the following line to the plugin options + +```js + icon: `src/images/icon.png`, // This path is relative to the root of the site. + icons: [ + { + src: `/favicons/android-chrome-192x192.png`, + sizes: `192x192`, + type: `image/png`, + }, + { + src: `/favicons/android-chrome-512x512.png`, + sizes: `512x512`, + type: `image/png`, + }, + ], // Add or remove icon sizes as desired +``` + +If you want to include more or fewer sizes, then the hybrid option is for you. Like automatic mode, you include a high resolution icon from which to generate smaller icons. But unlike automatic mode, you provide the `icons` array config and icons are generated based on the sizes defined in your config. Here's an example `gatsby-config.js`: + +The hybrid option allows the most flexibility while still not requiring you to create all icon sizes manually. + +#### Manual mode configuration + +Add the following line to the plugin options + +```js +icons: [ + { + src: `/favicons/android-chrome-192x192.png`, + sizes: `192x192`, + type: `image/png`, + }, + { + src: `/favicons/android-chrome-512x512.png`, + sizes: `512x512`, + type: `image/png`, + }, +], // Add or remove icon sizes as desired +``` + +In the manual mode, you are responsible for defining the entire web app manifest and providing the defined icons in the [static](https://www.gatsbyjs.org/docs/static-folder/) folder. Only icons you provide will be available. There is no automatic resizing done for you. + +### i18n – Multilang configuration + +```js +// in gatsby-config.js +module.exports = { + plugins: [ + { + resolve: `gatsby-plugin-manifest`, + options: { + manifests: [ + { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + start_url: `/de/`, + regex: `^/de/.*`, + language: `de`, + background_color: `#f7f0eb`, + theme_color: `#a2466c`, + display: `standalone`, + }, + { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + start_url: `/`, + regex: `.*`, + background_color: `#f7f0eb`, + theme_color: `#a2466c`, + display: `standalone`, + }, + ], + }, + }, + ], +} +``` + +You can add as many languages as you want, as well as mix and match auto, hybrid & manual icon modes. +Use basic composition like `Object.assign` or `rest/spread` operator (if supported by your Node version) to merge in shared configs so you don't have to repeat yourself. The above example could be written as: + +```js +// in gatsby-config.js +const sharedManifestOptions = { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + background_color: `#f7f0eb`, + theme_color: `#a2466c`, + display: `standalone`, +} +module.exports = { + plugins: [ + { + resolve: `gatsby-plugin-manifest`, + options: { + manifests: [ + Object.assign({}, sharedManifestOptions, { + start_url: `/de/`, + regex: `^/de/.*`, + language: `de`, + }), + Object.assign({}, sharedManifestOptions, { + start_url: `/`, + regex: `.*`, + }), + ], + }, + }, + ], +} +``` + +Make sure the default language comes last. +You don't have to specify a "default" language though, all pathes can start with a language, or you can tweak the regex to match whatever part of the `pathname` you like. + +### Feature configuration - **Optional** + +#### Iterative icon options + +The `icon_options` object may be used to iteratively add configuration items to the `icons` array. Any options included in this object will be merged with each object of the `icons` array (custom or default). Key value pairs already in the `icons` array will take precedence over duplicate items in the `icon_options` array. + +`icon_options` may be used as follows: + +```js +// in gatsby-config.js +module.exports = { + plugins: [ + { + resolve: `gatsby-plugin-manifest`, + options: { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + start_url: `/`, + background_color: `#f7f0eb`, + theme_color: `#a2466c`, + display: `standalone`, + icon: `src/images/icon.png`, + icon_options: { + // For all the options available, please see: + // https://developer.mozilla.org/en-US/docs/Web/Manifest + // https://w3c.github.io/manifest/#purpose-member + purpose: `maskable`, + }, + }, + }, + ], +} +``` + +#### Disable legacy icons + +iOS 11.3 added support for the web app manifest spec. Previous iOS versions won't recognize the icons defined in the webmanifest and the creation of `apple-touch-icon` links in `` is needed. This plugin creates them by default. If you don't want those icons to be generated you can set the `legacy` option to `false` in plugin configuration: + +```js +// in gatsby-config.js +module.exports = { + plugins: [ + { + resolve: `gatsby-plugin-manifest`, + options: { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + start_url: `/`, + background_color: `#f7f0eb`, + theme_color: `#a2466c`, + display: `standalone`, + icon: `src/images/icon.png`, + legacy: false, // this will not add apple-touch-icon links to + }, + }, + ], +} +``` + +#### Disable favicon + +Excludes `` link tag to html output. You can set `include_favicon` plugin option to `false` to opt-out of this behavior. + +```js +// in gatsby-config.js +module.exports = { + plugins: [ + { + resolve: `gatsby-plugin-manifest`, + options: { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + start_url: `/`, + background_color: `#f7f0eb`, + theme_color: `#a2466c`, + display: `standalone`, + icon: `src/images/icon.png`, // This path is relative to the root of the site. + include_favicon: false, // This will exclude favicon link tag + }, + }, + ], +} +``` + +#### Disable or configure "[cache busting](https://www.keycdn.com/support/what-is-cache-busting)" + +Cache Busting allows your updated icon to be quickly/easily visible to your sites visitors. HTTP caches could otherwise keep an old icon around for days and weeks. Cache busting can only done in 'automatic' and 'hybrid' modes. + +Cache busting works by calculating a unique "digest" of the provided icon and modifying links or file names of generated images with that unique digest. If you ever update your icon, the digest will change and caches will be busted. + +**Options:** + +- **\`query\`** - This is the default mode. File names are unmodified but a URL query is appended to all links. e.g. `icons/icon-48x48.png?digest=abc123` + +- **\`name\`** - Changes the cache busting mode to be done by file name. File names and links are modified with the icon digest. e.g. `icons/icon-48x48-abc123.png` (only needed if your CDN does not support URL query based cache busting) + +- **\`none\`** - Disables cache busting. File names and links remain unmodified. + +```js +// in gatsby-config.js +module.exports = { + plugins: [ + { + resolve: `gatsby-plugin-manifest`, + options: { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + start_url: `/`, + background_color: `#f7f0eb`, + theme_color: `#a2466c`, + display: `standalone`, + icon: `src/images/icon.png`, + cache_busting_mode: `none`, // `query`(default), `name`, or `none` + }, + }, + ], +} +``` + +#### Remove `theme-color` meta tag + +By default a `` tag is inserted into the html output. This can be problematic if you want to programmatically control that tag (e.g. when implementing light/dark themes in your project). You can set `theme_color_in_head` plugin option to `false` to opt-out of this behavior. + +```js +// in gatsby-config.js +module.exports = { + plugins: [ + { + resolve: `gatsby-plugin-manifest`, + options: { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + start_url: `/`, + background_color: `#f7f0eb`, + theme_color: `#a2466c`, + display: `standalone`, + icon: `src/images/icon.png`, + theme_color_in_head: false, // This will avoid adding theme-color meta tag. + }, + }, + ], +} +``` + +#### Enable CORS using `crossorigin` attribute + +Add a `crossorigin` attribute to the manifest `` link tag. + +You can set `crossOrigin` plugin option to `'use-credentials'` to enable sharing resources via cookies. Any invalid keyword or empty string will fallback to `'anonymous'`. + +You can find more information about `crossorigin` on [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes). + +```js +// in gatsby-config.js +module.exports = { + plugins: [ + { + resolve: `gatsby-plugin-manifest`, + options: { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + start_url: `/`, + background_color: `#f7f0eb`, + theme_color: `#a2466c`, + display: `standalone`, + icon: `src/images/icon.png`, + crossOrigin: `use-credentials`, // `use-credentials` or `anonymous` + }, + }, + ], +} +``` + +## Appendices + +Additional information that may be interesting or valuable. + +### Default icon config + +When in automatic mode the following json array is injected into the manifest configuration you provide and the icons are generated from it. + +```json +[ + { + "src": "icons/icon-48x48.png", + "sizes": "48x48", + "type": "image/png" + }, + { + "src": "icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/icon-256x256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "icons/icon-384x384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } +] +``` + +### Legacy icon support coverage + +Currently this feature only covers older versions of [iOS Safari](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html). + +Internet Explorer is the only other major browser that doesn't support the web app manifest, and it's market share is so small no one has contributed support. + +### Additional resources + +This article from the Chrome DevRel team is a good intro to the web app +manifest—https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ + +For more information see the w3 spec https://www.w3.org/TR/appmanifest/ or Mozilla docs https://developer.mozilla.org/en-US/docs/Web/Manifest. diff --git a/packages/gatsby-plugin-manifest/common.js b/packages/gatsby-plugin-manifest/common.js index 682f845..008633c 100644 --- a/packages/gatsby-plugin-manifest/common.js +++ b/packages/gatsby-plugin-manifest/common.js @@ -1,62 +1,78 @@ -const fs = require(`fs`) +"use strict"; -// default icons for generating icons -exports.defaultIcons = [ - { - src: `icons/icon-48x48.png`, - sizes: `48x48`, - type: `image/png`, - }, - { - src: `icons/icon-72x72.png`, - sizes: `72x72`, - type: `image/png`, - }, - { - src: `icons/icon-96x96.png`, - sizes: `96x96`, - type: `image/png`, - }, - { - src: `icons/icon-144x144.png`, - sizes: `144x144`, - type: `image/png`, - }, - { - src: `icons/icon-192x192.png`, - sizes: `192x192`, - type: `image/png`, - }, - { - src: `icons/icon-256x256.png`, - sizes: `256x256`, - type: `image/png`, - }, - { - src: `icons/icon-384x384.png`, - sizes: `384x384`, - type: `image/png`, - }, - { - src: `icons/icon-512x512.png`, - sizes: `512x512`, - type: `image/png`, - }, -] +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); + +var _fs = _interopRequireDefault(require("fs")); +var _path = _interopRequireDefault(require("path")); + +// default icons for generating icons +exports.defaultIcons = [{ + src: "icons/icon-48x48.png", + sizes: "48x48", + type: "image/png" +}, { + src: "icons/icon-72x72.png", + sizes: "72x72", + type: "image/png" +}, { + src: "icons/icon-96x96.png", + sizes: "96x96", + type: "image/png" +}, { + src: "icons/icon-144x144.png", + sizes: "144x144", + type: "image/png" +}, { + src: "icons/icon-192x192.png", + sizes: "192x192", + type: "image/png" +}, { + src: "icons/icon-256x256.png", + sizes: "256x256", + type: "image/png" +}, { + src: "icons/icon-384x384.png", + sizes: "384x384", + type: "image/png" +}, { + src: "icons/icon-512x512.png", + sizes: "512x512", + type: "image/png" +}]; /** * Check if the icon exists on the filesystem * * @param {String} srcIcon Path of the icon */ + exports.doesIconExist = function doesIconExist(srcIcon) { try { - return fs.statSync(srcIcon).isFile() + return _fs.default.statSync(srcIcon).isFile(); } catch (e) { - if (e.code === `ENOENT`) { - return false - } else { - throw e + if (e.code !== "ENOENT") { + throw e; } + + return false; } -} +}; +/** + * @param {string} path The generic path to an icon + * @param {string} digest The digest of the icon provided in the plugin's options. + */ + + +exports.addDigestToPath = function (path, digest, method) { + if (method === "name") { + var parsedPath = _path.default.parse(path); + + return parsedPath.dir + "/" + parsedPath.name + "-" + digest + parsedPath.ext; + } + + if (method === "query") { + return path + "?v=" + digest; + } + + return path; +}; \ No newline at end of file diff --git a/packages/gatsby-plugin-manifest/gatsby-node.js b/packages/gatsby-plugin-manifest/gatsby-node.js index c8b6765..d1a38eb 100644 --- a/packages/gatsby-plugin-manifest/gatsby-node.js +++ b/packages/gatsby-plugin-manifest/gatsby-node.js @@ -1,95 +1,251 @@ -const fs = require(`fs`) -const path = require(`path`) -const Promise = require(`bluebird`) -const sharp = require(`sharp`) -const { defaultIcons, doesIconExist } = require(`./common.js`) +"use strict"; -sharp.simd(true) +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); -function generateIcons(icons, srcIcon) { - return Promise.map(icons, icon => { - const size = parseInt(icon.sizes.substring(0, icon.sizes.lastIndexOf(`x`))) - const imgPath = path.join(`public`, icon.src) - - return sharp(srcIcon) - .resize(size) - .toFile(imgPath) - .then(() => {}) - }) +var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); + +var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose")); + +var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); + +var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); + +var _fs = _interopRequireDefault(require("fs")); + +var _path = _interopRequireDefault(require("path")); + +var _sharp = _interopRequireDefault(require("sharp")); + +var _createContentDigest = _interopRequireDefault(require("gatsby/dist/utils/create-content-digest")); + +var _common = require("./common"); + +_sharp.default.simd(true); + +try { + // Handle Sharp's concurrency based on the Gatsby CPU count + // See: http://sharp.pixelplumbing.com/en/stable/api-utility/#concurrency + // See: https://www.gatsbyjs.org/docs/multi-core-builds/ + var cpuCoreCount = require("gatsby/dist/utils/cpu-core-count"); + + _sharp.default.concurrency(cpuCoreCount()); +} catch (_unused) {// if above throws error this probably means that used Gatsby version + // doesn't support cpu-core-count utility. } -const writeManifest = ({ language, ...manifest }) => { - const suffix = language ? `_${language}` : `` - fs.writeFileSync( - path.join(`public`, `manifest${suffix}.webmanifest`), - JSON.stringify(manifest) - ) +function generateIcons(icons, srcIcon) { + return Promise.all(icons.map( + /*#__PURE__*/ + function () { + var _ref = (0, _asyncToGenerator2.default)( + /*#__PURE__*/ + _regenerator.default.mark(function _callee(icon) { + var size, imgPath, density; + return _regenerator.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + size = parseInt(icon.sizes.substring(0, icon.sizes.lastIndexOf("x"))); + imgPath = _path.default.join("public", icon.src); // For vector graphics, instruct sharp to use a pixel density + // suitable for the resolution we're rasterizing to. + // For pixel graphics sources this has no effect. + // Sharp accept density from 1 to 2400 + + density = Math.min(2400, Math.max(1, size)); + return _context.abrupt("return", (0, _sharp.default)(srcIcon, { + density: density + }).resize({ + width: size, + height: size, + fit: "contain", + background: { + r: 255, + g: 255, + b: 255, + alpha: 0 + } + }).toFile(imgPath)); + + case 4: + case "end": + return _context.stop(); + } + } + }, _callee, this); + })); + + return function (_x) { + return _ref.apply(this, arguments); + }; + }())); } -const makeManifest = ({ icon, ...manifest }) => - new Promise((resolve, reject) => { - // If icons are not manually defined, use the default icon set. - if (!manifest.icons) { - manifest.icons = defaultIcons - } - - // Determine destination path for icons. - const iconPath = path.join(`public`, path.dirname(manifest.icons[0].src)) - - //create destination directory if it doesn't exist - if (!fs.existsSync(iconPath)) { - fs.mkdirSync(iconPath) - } - - writeManifest(manifest) - - fs.writeFileSync( - path.join(`public`, `browserconfig.xml`), - ` - - - - ${manifest.background_color} - ${manifest.icons - .map(x => { - const { sizes, src } = x - return `` - }) - .join(`\n `)} - - -` - ) - - // Only auto-generate icons if a src icon is defined. - if (icon !== undefined) { - // Check if the icon exists - if (!doesIconExist(icon)) { - reject( - `icon (${icon}) does not exist as defined in gatsby-config.js. Make sure the file exists relative to the root of the site.` - ) +exports.onPostBootstrap = +/*#__PURE__*/ +function () { + var _ref4 = (0, _asyncToGenerator2.default)( + /*#__PURE__*/ + _regenerator.default.mark(function _callee2(_ref2, _ref3) { + var reporter, manifests, manifest, activity; + return _regenerator.default.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + reporter = _ref2.reporter; + manifests = _ref3.manifests, manifest = (0, _objectWithoutPropertiesLoose2.default)(_ref3, ["manifests"]); + activity = reporter.activityTimer("Build manifest and related icons"); + activity.start(); + + if (!Array.isArray(manifests)) { + _context2.next = 9; + break; + } + + _context2.next = 7; + return Promise.all(manifests.map(function (x) { + return makeManifest(reporter, x); + })); + + case 7: + _context2.next = 11; + break; + + case 9: + _context2.next = 11; + return makeManifest(reporter, manifest); + + case 11: + activity.end(); + + case 12: + case "end": + return _context2.stop(); + } + } + }, _callee2, this); + })); + + return function (_x2, _x3) { + return _ref4.apply(this, arguments); + }; +}(); + +var makeManifest = +/*#__PURE__*/ +function () { + var _ref5 = (0, _asyncToGenerator2.default)( + /*#__PURE__*/ + _regenerator.default.mark(function _callee3(reporter, pluginOptions) { + var icon, language, manifest, suffix, paths, sharpIcon, metadata, cacheMode, iconDigest; + return _regenerator.default.wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + icon = pluginOptions.icon, language = pluginOptions.language, manifest = (0, _objectWithoutPropertiesLoose2.default)(pluginOptions, ["icon", "language"]); + suffix = language ? "_" + language : ""; // Delete options we won't pass to the manifest.webmanifest. + + delete manifest.plugins; + delete manifest.legacy; + delete manifest.theme_color_in_head; + delete manifest.cache_busting_mode; + delete manifest.crossOrigin; + delete manifest.icon_options; + delete manifest.include_favicon; + delete manifest.regex; // If icons are not manually defined, use the default icon set. + + if (!manifest.icons) { + manifest.icons = _common.defaultIcons; + } // Specify extra options for each icon (if requested). + + + if (pluginOptions.icon_options) { + manifest.icons = manifest.icons.map(function (icon) { + return (0, _extends2.default)({}, pluginOptions.icon_options, icon); + }); + } // Determine destination path for icons. + + + paths = {}; + manifest.icons.forEach(function (icon) { + var iconPath = _path.default.join("public", _path.default.dirname(icon.src)); + + if (!paths[iconPath]) { + var exists = _fs.default.existsSync(iconPath); //create destination directory if it doesn't exist + + + if (!exists) { + _fs.default.mkdirSync(iconPath); + } + + paths[iconPath] = true; + } + }); // Only auto-generate icons if a src icon is defined. + + if (!(icon !== undefined)) { + _context3.next = 30; + break; + } + + if ((0, _common.doesIconExist)(icon)) { + _context3.next = 17; + break; + } + + throw "icon (" + icon + ") does not exist as defined in gatsby-config.js. Make sure the file exists relative to the root of the site."; + + case 17: + sharpIcon = (0, _sharp.default)(icon); + _context3.next = 20; + return sharpIcon.metadata(); + + case 20: + metadata = _context3.sent; + + if (metadata.width !== metadata.height) { + reporter.warn("The icon(" + icon + ") you provided to 'gatsby-plugin-manifest' is not square.\n" + "The icons we generate will be square and for the best results we recommend you provide a square icon.\n"); + } //add cache busting + + + cacheMode = typeof pluginOptions.cache_busting_mode !== "undefined" ? pluginOptions.cache_busting_mode : "query"; //if cacheBusting is being done via url query icons must be generated before cache busting runs + + if (!(cacheMode === "query")) { + _context3.next = 26; + break; + } + + _context3.next = 26; + return generateIcons(manifest.icons, icon); + + case 26: + if (cacheMode !== "none") { + iconDigest = (0, _createContentDigest.default)(_fs.default.readFileSync(icon)); + manifest.icons.forEach(function (icon) { + icon.src = (0, _common.addDigestToPath)(icon.src, iconDigest, cacheMode); + }); + } //if file names are being modified by cacheBusting icons must be generated after cache busting runs + + + if (!(cacheMode !== "query")) { + _context3.next = 30; + break; + } + + _context3.next = 30; + return generateIcons(manifest.icons, icon); + + case 30: + //Write manifest + _fs.default.writeFileSync(_path.default.join("public", "manifest" + suffix + ".webmanifest"), JSON.stringify(manifest)); + + case 31: + case "end": + return _context3.stop(); + } } - generateIcons(manifest.icons, icon).then(() => { - //images have been generated - console.log(`done generating icons for manifest`) - resolve() - }) - } else { - resolve() - } - }) - -exports.onPostBootstrap = (args, { manifests, ...pluginOptions }) => - new Promise((resolve, reject) => { - delete pluginOptions.plugins - - if (Array.isArray(manifests)) { - Promise.all(manifests.map(makeManifest)) - .then(resolve) - .catch(resolve) - } else { - makeManifest(pluginOptions) - .then(resolve) - .catch(resolve) - } - }) + }, _callee3, this); + })); + + return function makeManifest(_x4, _x5) { + return _ref5.apply(this, arguments); + }; +}(); \ No newline at end of file diff --git a/packages/gatsby-plugin-manifest/gatsby-ssr.js b/packages/gatsby-plugin-manifest/gatsby-ssr.js index d59486c..4ea48eb 100644 --- a/packages/gatsby-plugin-manifest/gatsby-ssr.js +++ b/packages/gatsby-plugin-manifest/gatsby-ssr.js @@ -1,66 +1,110 @@ -import React from 'react' -import { withPrefix } from 'gatsby' +"use strict"; -export const onRenderBody = ( - { pathname = `/`, setHeadComponents }, - pluginOptions -) => { - let manifest = pluginOptions +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); + +var _react = _interopRequireDefault(require("react")); + +var _gatsby = require("gatsby"); + +var _createContentDigest = _interopRequireDefault(require("gatsby/dist/utils/create-content-digest")); + +var _common = require("./common.js"); + +var _fs = _interopRequireDefault(require("fs")); + +var _jsxFileName = "/Users/Can/Projekte/Coding/forks/gatsby/packages/gatsby-plugin-manifest/src/gatsby-ssr.js"; +var iconDigest = null; + +exports.onRenderBody = function (_ref, pluginOptions) { + var setHeadComponents = _ref.setHeadComponents, + _ref$pathname = _ref.pathname, + pathname = _ref$pathname === void 0 ? "/" : _ref$pathname; if (Array.isArray(pluginOptions.manifests)) { - // manifest = pluginOptions.find(x => RegExp(x.pattern).test(pathname)) - manifest = pluginOptions.manifests.find(x => - RegExp(`^/${x.language}/.*`).test(pathname) - ) - } + pluginOptions = pluginOptions.manifests.find(function (x) { + return RegExp(x.regex || "^/" + x.language + "/.*").test(pathname); + }); + if (!pluginOptions) return false; + } // We use this to build a final array to pass as the argument to setHeadComponents at the end of onRenderBody. + - if (!manifest) { - return console.log( - `missing manifest`, - pathname, - pluginOptions.manifests.find(x => - RegExp(`^/${x.language}/.*`).test(pathname) - ) - ) + var headComponents = []; + var srcIconExists = !!pluginOptions.icon; + var icons = pluginOptions.icons || _common.defaultIcons; + var legacy = typeof pluginOptions.legacy !== "undefined" ? pluginOptions.legacy : true; + var cacheBusting = typeof pluginOptions.cache_busting_mode !== "undefined" ? pluginOptions.cache_busting_mode : "query"; // If icons were generated, also add a favicon link. + + if (srcIconExists) { + var favicon = icons && icons.length ? icons[0].src : null; + + if (cacheBusting !== "none") { + iconDigest = (0, _createContentDigest.default)(_fs.default.readFileSync(pluginOptions.icon)); + } + + var insertFaviconLinkTag = typeof pluginOptions.include_favicon !== "undefined" ? pluginOptions.include_favicon : true; + + if (favicon && insertFaviconLinkTag) { + headComponents.push(_react.default.createElement("link", { + key: "gatsby-plugin-manifest-icon-link", + rel: "shortcut icon", + href: (0, _gatsby.withPrefix)((0, _common.addDigestToPath)(favicon, iconDigest, cacheBusting)), + __source: { + fileName: _jsxFileName, + lineNumber: 49 + }, + __self: this + })); + } } - const { icon, icons, theme_color, language } = manifest + var suffix = pluginOptions.language ? "_" + pluginOptions.language : ""; // Add manifest link tag. - // If icons were generated, also add a favicon link. - if (icon) { - let favicon = `/icons/icon-48x48.png` + headComponents.push(_react.default.createElement("link", { + key: "gatsby-plugin-manifest-link", + rel: "manifest", + href: (0, _gatsby.withPrefix)("/manifest" + suffix + ".webmanifest"), + crossOrigin: pluginOptions.crossOrigin, + __source: { + fileName: _jsxFileName, + lineNumber: 62 + }, + __self: this + })); // The user has an option to opt out of the theme_color meta tag being inserted into the head. - // The icon path could be different in hybrid mode - // this takes the first one of the possible icons - if (icons && icons.length) { - favicon = icons[0].src + if (pluginOptions.theme_color) { + var insertMetaTag = typeof pluginOptions.theme_color_in_head !== "undefined" ? pluginOptions.theme_color_in_head : true; + + if (insertMetaTag) { + headComponents.push(_react.default.createElement("meta", { + key: "gatsby-plugin-manifest-meta", + name: "theme-color", + content: pluginOptions.theme_color, + __source: { + fileName: _jsxFileName, + lineNumber: 79 + }, + __self: this + })); } + } - setHeadComponents([ - , - ]) + if (legacy) { + var iconLinkTags = icons.map(function (icon) { + return _react.default.createElement("link", { + key: "gatsby-plugin-manifest-apple-touch-icon-" + icon.sizes, + rel: "apple-touch-icon", + sizes: icon.sizes, + href: (0, _gatsby.withPrefix)((0, _common.addDigestToPath)(icon.src, iconDigest, srcIconExists ? cacheBusting : "none")), + __source: { + fileName: _jsxFileName, + lineNumber: 90 + }, + __self: this + }); + }); + headComponents = headComponents.concat(iconLinkTags); } - const manifestFilename = language - ? `/manifest_${language}.webmanifest` - : `/manifest.webmanifest` - - setHeadComponents([ - , - , - ]) - - return true -} + setHeadComponents(headComponents); + return true; +}; \ No newline at end of file diff --git a/packages/gatsby-plugin-manifest/package.json b/packages/gatsby-plugin-manifest/package.json index 3163824..21cb58c 100644 --- a/packages/gatsby-plugin-manifest/package.json +++ b/packages/gatsby-plugin-manifest/package.json @@ -1,15 +1,20 @@ { "name": "@gaiama/gatsby-plugin-manifest", "description": "Gatsby plugin which adds a manifest.webmanifest to make sites progressive web apps", - "version": "2.0.5", + "version": "2.0.29", "author": "Kyle Mathews ", "bugs": { "url": "https://github.com/gatsbyjs/gatsby/issues" }, "dependencies": { - "@babel/runtime": "^7.3.4", - "bluebird": "^3.5.4", - "sharp": "^0.22.0" + "@babel/runtime": "^7.0.0", + "sharp": "^0.21.3" + }, + "devDependencies": { + "@babel/cli": "^7.0.0", + "@babel/core": "^7.0.0", + "babel-preset-gatsby-package": "^0.1.4", + "cross-env": "^5.1.4" }, "homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest#readme", "keywords": [ @@ -24,7 +29,7 @@ "license": "MIT", "main": "index.js", "peerDependencies": { - "gatsby": ">2.0.0-alpha" + "gatsby": "^2.0.15" }, "repository": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-manifest", "scripts": { diff --git a/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/common.js.snap b/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/common.js.snap new file mode 100644 index 0000000..340572e --- /dev/null +++ b/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/common.js.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`gatsby-plugin-manifest defaultIcons includes all icon sizes 1`] = ` +Array [ + Object { + "sizes": "48x48", + "src": "icons/icon-48x48.png", + "type": "image/png", + }, + Object { + "sizes": "72x72", + "src": "icons/icon-72x72.png", + "type": "image/png", + }, + Object { + "sizes": "96x96", + "src": "icons/icon-96x96.png", + "type": "image/png", + }, + Object { + "sizes": "144x144", + "src": "icons/icon-144x144.png", + "type": "image/png", + }, + Object { + "sizes": "192x192", + "src": "icons/icon-192x192.png", + "type": "image/png", + }, + Object { + "sizes": "256x256", + "src": "icons/icon-256x256.png", + "type": "image/png", + }, + Object { + "sizes": "384x384", + "src": "icons/icon-384x384.png", + "type": "image/png", + }, + Object { + "sizes": "512x512", + "src": "icons/icon-512x512.png", + "type": "image/png", + }, +] +`; 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 new file mode 100644 index 0000000..3f6f3d8 --- /dev/null +++ b/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-node.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test plugin manifest options correctly works with default parameters 1`] = `"{\\"name\\":\\"GatsbyJS\\",\\"short_name\\":\\"GatsbyJS\\",\\"start_url\\":\\"/\\",\\"background_color\\":\\"#f7f0eb\\",\\"theme_color\\":\\"#a2466c\\",\\"display\\":\\"standalone\\",\\"icons\\":[{\\"src\\":\\"icons/icon-48x48.png\\",\\"sizes\\":\\"48x48\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-72x72.png\\",\\"sizes\\":\\"72x72\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-96x96.png\\",\\"sizes\\":\\"96x96\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-144x144.png\\",\\"sizes\\":\\"144x144\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-192x192.png\\",\\"sizes\\":\\"192x192\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-256x256.png\\",\\"sizes\\":\\"256x256\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-384x384.png\\",\\"sizes\\":\\"384x384\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-512x512.png\\",\\"sizes\\":\\"512x512\\",\\"type\\":\\"image/png\\"}]}"`; 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 new file mode 100644 index 0000000..b8d2b4b --- /dev/null +++ b/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-ssr.js.snap @@ -0,0 +1,560 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`gatsby-plugin-manifest CORS Generation Add crossOrigin when 'crossOrigin' is anonymous 1`] = ` +Array [ + , +] +`; + +exports[`gatsby-plugin-manifest CORS Generation Adds a crossOrigin attribute to manifest link tag if provided 1`] = ` +Array [ + , +] +`; + +exports[`gatsby-plugin-manifest CORS Generation Does not add crossOrigin when 'crossOrigin' is blank 1`] = ` +Array [ + , +] +`; + +exports[`gatsby-plugin-manifest Cache Busting Does file name cache busting if "cache_busting_mode" option is set to name 1`] = ` +Array [ + , + , + , + , + , + , + , + , + , + , +] +`; + +exports[`gatsby-plugin-manifest Cache Busting Does query cache busting if "cache_busting_mode" option is set to query 1`] = ` +Array [ + , + , + , + , + , + , + , + , + , + , +] +`; + +exports[`gatsby-plugin-manifest Cache Busting Does query cache busting if "cache_busting_mode" option is set to undefined 1`] = ` +Array [ + , + , + , + , + , + , + , + , + , + , +] +`; + +exports[`gatsby-plugin-manifest Cache Busting doesn't add cache busting if "cache_busting_mode" option is set to none 1`] = ` +Array [ + , + , + , + , + , + , + , + , + , + , +] +`; + +exports[`gatsby-plugin-manifest Cache Busting doesn't add cache busting in manual mode 1`] = ` +Array [ + , + , + , +] +`; + +exports[`gatsby-plugin-manifest Favicon Adds link favicon tag if "include_favicon" is set to true 1`] = ` +Array [ + , + , +] +`; + +exports[`gatsby-plugin-manifest Favicon Does not add a link favicon if "include_favicon" option is set to false 1`] = ` +Array [ + , +] +`; + +exports[`gatsby-plugin-manifest Favicon Does not add a link favicon if in manual mode 1`] = ` +Array [ + , +] +`; + +exports[`gatsby-plugin-manifest Legacy Icons Does create legacy links if "legacy" not specified in automatic mode 1`] = ` +Array [ + , + , + , + , + , + , + , + , + , +] +`; + +exports[`gatsby-plugin-manifest Legacy Icons Does create legacy links if "legacy" not specified in hybrid mode. 1`] = ` +Array [ + , + , + , +] +`; + +exports[`gatsby-plugin-manifest Legacy Icons Does create legacy links if "legacy" not specified in manual mode. 1`] = ` +Array [ + , + , + , +] +`; + +exports[`gatsby-plugin-manifest Legacy Icons Does not create legacy links If "legacy" options is false and in automatic 1`] = ` +Array [ + , +] +`; + +exports[`gatsby-plugin-manifest Legacy Icons Does not create legacy links If "legacy" options is false and in hybrid mode 1`] = ` +Array [ + , +] +`; + +exports[`gatsby-plugin-manifest Legacy Icons Does not create legacy links If "legacy" options is false and in manual mode 1`] = ` +Array [ + , + , +] +`; + +exports[`gatsby-plugin-manifest Manifest Link Generation Add a "theme color" meta tag if "theme_color_in_head" is set to true 1`] = ` +Array [ + , + , +] +`; + +exports[`gatsby-plugin-manifest Manifest Link Generation Adds "shortcut icon" and "manifest" links and "theme_color" meta tag to head 1`] = ` +Array [ + , + , + , + , + , + , + , + , + , + , + , +] +`; + +exports[`gatsby-plugin-manifest Manifest Link Generation Adds a "theme color" meta tag to head if "theme_color_in_head" is not provided 1`] = ` +Array [ + , + , +] +`; + +exports[`gatsby-plugin-manifest Manifest Link Generation Adds correct i18n "manifest" link to head 1`] = ` +Array [ + , + , + , + , + , + , + , + , + , +] +`; + +exports[`gatsby-plugin-manifest Manifest Link Generation Does not add a "theme color" meta tag if "theme_color_in_head" is set to false 1`] = ` +Array [ + , +] +`; + +exports[`gatsby-plugin-manifest Manifest Link Generation Does not add a "theme_color" meta tag to head if "theme_color" option is not provided. 1`] = ` +Array [ + , +] +`; diff --git a/packages/gatsby-plugin-manifest/src/__tests__/common.js b/packages/gatsby-plugin-manifest/src/__tests__/common.js new file mode 100644 index 0000000..3c78932 --- /dev/null +++ b/packages/gatsby-plugin-manifest/src/__tests__/common.js @@ -0,0 +1,42 @@ +const path = require(`path`) +const { defaultIcons, doesIconExist, addDigestToPath } = require(`../common`) + +describe(`gatsby-plugin-manifest`, () => { + describe(`defaultIcons`, () => { + it(`includes all icon sizes`, () => { + expect(defaultIcons).toMatchSnapshot() + }) + }) + + describe(`doesIconExist`, () => { + it(`returns true if image exists on filesystem`, () => { + const iconSrc = path.resolve(__dirname, `./images/gatsby-logo.png`) + expect(doesIconExist(iconSrc)).toBeTruthy() + }) + + it(`returns false if image does not exist on filesystem`, () => { + const iconSrc = path.resolve(__dirname, `./images/non-existent-logo.png`) + expect(doesIconExist(iconSrc)).toBeFalsy() + }) + }) + + describe(`addDigestToPath`, () => { + it(`returns unmodified path`, () => { + expect( + addDigestToPath(`icons/icon-48x48.png`, `thisismydigest`, `none`) + ).toBe(`icons/icon-48x48.png`) + }) + + it(`returns query modified path`, () => { + expect( + addDigestToPath(`icons/icon-48x48.png`, `thisismydigest`, `query`) + ).toBe(`icons/icon-48x48.png?v=thisismydigest`) + }) + + it(`returns fileName modified path`, () => { + expect( + addDigestToPath(`icons/icon-48x48.png`, `thisismydigest`, `name`) + ).toBe(`icons/icon-48x48-thisismydigest.png`) + }) + }) +}) diff --git a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js new file mode 100644 index 0000000..fd4a537 --- /dev/null +++ b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js @@ -0,0 +1,322 @@ +jest.mock(`fs`, () => { + return { + existsSync: jest.fn().mockImplementation(() => true), + writeFileSync: jest.fn(), + mkdirSync: jest.fn(), + readFileSync: jest.fn().mockImplementation(() => `someIconImage`), + statSync: jest.fn(), + } +}) +/* + * We mock sharp because it depends on fs implementation (which is mocked) + * this causes test failures, so mock it to avoid + */ + +jest.mock(`sharp`, () => { + let sharp = jest.fn( + () => + new class { + resize() { + return this + } + toFile() { + return Promise.resolve() + } + metadata() { + return { + width: 128, + height: 128, + } + } + }() + ) + + sharp.simd = jest.fn() + sharp.concurrency = jest.fn() + + return sharp +}) + +jest.mock(`gatsby/dist/utils/create-content-digest`, () => + jest.fn(() => `contentDigest`) +) + +const fs = require(`fs`) +const path = require(`path`) +const sharp = require(`sharp`) +const reporter = { + activityTimer: jest.fn().mockImplementation(function() { + return { + start: jest.fn(), + end: jest.fn(), + } + }), +} +const { onPostBootstrap } = require(`../gatsby-node`) + +const apiArgs = { + reporter, +} + +const manifestOptions = { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + start_url: `/`, + background_color: `#f7f0eb`, + theme_color: `#a2466c`, + display: `standalone`, + icons: [ + { + src: `icons/icon-48x48.png`, + sizes: `48x48`, + type: `image/png`, + purpose: `all`, + }, + { + src: `icons/icon-128x128.png`, + sizes: `128x128`, + type: `image/png`, + }, + ], +} + +describe(`Test plugin manifest options`, () => { + beforeEach(() => { + fs.writeFileSync.mockReset() + fs.mkdirSync.mockReset() + fs.existsSync.mockReset() + sharp.mockClear() + }) + + // the require of gatsby-node performs the invoking + it(`invokes sharp.simd for optimization`, () => { + expect(sharp.simd).toHaveBeenCalledTimes(1) + }) + + it(`correctly works with default parameters`, async () => { + await onPostBootstrap(apiArgs, { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + start_url: `/`, + background_color: `#f7f0eb`, + theme_color: `#a2466c`, + display: `standalone`, + }) + const contents = fs.writeFileSync.mock.calls[0][1] + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(`public`, `manifest.webmanifest`), + expect.anything() + ) + expect(sharp).toHaveBeenCalledTimes(0) + expect(contents).toMatchSnapshot() + }) + + it(`correctly works with multiple icon paths`, async () => { + fs.existsSync.mockReturnValue(false) + + const size = 48 + + const pluginSpecificOptions = { + icons: [ + { + src: `icons/icon-48x48.png`, + sizes: `${size}x${size}`, + type: `image/png`, + }, + { + src: `other-icons/icon-48x48.png`, + sizes: `${size}x${size}`, + type: `image/png`, + }, + ], + } + + await onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }) + + const firstIconPath = path.join( + `public`, + path.dirname(`icons/icon-48x48.png`) + ) + const secondIconPath = path.join( + `public`, + path.dirname(`other-icons/icon-48x48.png`) + ) + + expect(fs.mkdirSync).toHaveBeenNthCalledWith(1, firstIconPath) + expect(fs.mkdirSync).toHaveBeenNthCalledWith(2, secondIconPath) + }) + + it(`invokes sharp if icon argument specified`, 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`, + }, + ], + } + + await onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }) + + expect(sharp).toHaveBeenCalledWith(icon, { density: size }) + expect(sharp).toHaveBeenCalledTimes(2) + }) + + it(`fails on non existing icon`, async () => { + fs.statSync.mockReturnValueOnce({ isFile: () => false }) + + const pluginSpecificOptions = { + icon: `non/existing/path`, + } + + return onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }).catch(err => { + expect(sharp).toHaveBeenCalledTimes(0) + expect(err).toBe( + `icon (non/existing/path) does not exist as defined in gatsby-config.js. Make sure the file exists relative to the root of the site.` + ) + }) + }) + + it(`doesn't write extra properties to manifest`, async () => { + const pluginSpecificOptions = { + icon: undefined, + legacy: true, + plugins: [], + theme_color_in_head: false, + cache_busting_mode: `name`, + include_favicon: true, + crossOrigin: `anonymous`, + icon_options: {}, + } + await onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }) + + expect(sharp).toHaveBeenCalledTimes(0) + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.anything(), + JSON.stringify(manifestOptions) + ) + }) + + it(`does file name based cache busting`, async () => { + fs.statSync.mockReturnValueOnce({ isFile: () => true }) + + const pluginSpecificOptions = { + icon: `images/gatsby-logo.png`, + legacy: true, + cache_busting_mode: `name`, + } + await onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }) + + expect(sharp).toHaveBeenCalledTimes(3) + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.anything(), + JSON.stringify(manifestOptions) + ) + }) + + it(`does not do cache cache busting`, async () => { + fs.statSync.mockReturnValueOnce({ isFile: () => true }) + + const pluginSpecificOptions = { + icon: `images/gatsby-logo.png`, + legacy: true, + cache_busting_mode: `none`, + } + await onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }) + + expect(sharp).toHaveBeenCalledTimes(3) + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.anything(), + JSON.stringify(manifestOptions) + ) + }) + + it(`icon options iterator adds options and the icon array take precedence`, async () => { + fs.statSync.mockReturnValueOnce({ isFile: () => true }) + + const pluginSpecificOptions = { + icon: `images/gatsby-logo.png`, + icon_options: { + purpose: `maskable`, + }, + } + await onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }) + + expect(sharp).toHaveBeenCalledTimes(3) + const content = JSON.parse(fs.writeFileSync.mock.calls[0][1]) + expect(content.icons[0].purpose).toEqual(`all`) + expect(content.icons[1].purpose).toEqual(`maskable`) + }) + + it(`generates all language versions`, async () => { + fs.statSync.mockReturnValueOnce({ isFile: () => true }) + const pluginSpecificOptions = { + manifests: [ + { + ...manifestOptions, + start_url: `/de/`, + regex: `^/de/.*`, + language: `de`, + }, + { + ...manifestOptions, + start_url: `/en/`, + regex: `^/en/.*`, + language: `en`, + }, + { + ...manifestOptions, + start_url: `/es/`, + regex: `^/es/.*`, + language: `es`, + }, + ], + } + const expectedResults = pluginSpecificOptions.manifests.map( + ({ regex, language, ...x }) => x + ) + + await onPostBootstrap(apiArgs, pluginSpecificOptions) + + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.anything(), + JSON.stringify(expectedResults[0]) + ) + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.anything(), + JSON.stringify(expectedResults[1]) + ) + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.anything(), + JSON.stringify(expectedResults[2]) + ) + }) +}) diff --git a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js new file mode 100644 index 0000000..1a2df18 --- /dev/null +++ b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js @@ -0,0 +1,352 @@ +jest.mock(`fs`, () => { + return { + readFileSync: jest.fn().mockImplementation(() => `someIconImage`), + } +}) + +jest.mock(`gatsby/dist/utils/create-content-digest`, () => + jest.fn(() => `contentDigest`) +) + +const { onRenderBody } = require(`../gatsby-ssr`) + +let headComponents +const setHeadComponents = args => (headComponents = headComponents.concat(args)) + +const ssrArgs = { + setHeadComponents, + pathname: `/`, +} + +describe(`gatsby-plugin-manifest`, () => { + beforeEach(() => { + global.__PATH_PREFIX__ = `` + headComponents = [] + }) + + it(`Creates href attributes using pathPrefix`, () => { + global.__PATH_PREFIX__ = `/path-prefix` + + onRenderBody(ssrArgs, { + icon: true, + theme_color: `#000000`, + }) + + headComponents + .filter(component => component.type === `link`) + .forEach(component => { + expect(component.props.href).toEqual( + expect.stringMatching(/^\/path-prefix\//) + ) + }) + }) + + describe(`Manifest Link Generation`, () => { + it(`Adds a "theme color" meta tag to head if "theme_color_in_head" is not provided`, () => { + onRenderBody(ssrArgs, { + theme_color: `#000000`, + include_favicon: false, + cache_busting_mode: false, + legacy: false, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Add a "theme color" meta tag if "theme_color_in_head" is set to true`, () => { + onRenderBody(ssrArgs, { + theme_color: `#000000`, + theme_color_in_head: true, + include_favicon: false, + cache_busting_mode: false, + legacy: false, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Does not add a "theme color" meta tag if "theme_color_in_head" is set to false`, () => { + onRenderBody(ssrArgs, { + theme_color: `#000000`, + theme_color_in_head: false, + include_favicon: false, + cache_busting_mode: false, + legacy: false, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Does not add a "theme_color" meta tag to head if "theme_color" option is not provided.`, () => { + onRenderBody(ssrArgs, { + icon: true, + include_favicon: false, + cache_busting_mode: false, + legacy: false, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Adds "shortcut icon" and "manifest" links and "theme_color" meta tag to head`, () => { + onRenderBody(ssrArgs, { + icon: true, + theme_color: `#000000`, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Adds correct i18n "manifest" link to head`, () => { + const args = { + ...ssrArgs, + pathname: `/es/sobre-nosotros`, + } + onRenderBody(args, { + manifests: [ + { + start_url: `/de/`, + regex: `^/de/.*`, + language: `de`, + }, + { + start_url: `/en/`, + regex: `^/en/.*`, + language: `en`, + }, + { + start_url: `/es/`, + regex: `^/es/.*`, + language: `es`, + }, + ], + }) + expect(headComponents).toMatchSnapshot() + }) + }) + + describe(`Legacy Icons`, () => { + describe(`Does create legacy links`, () => { + it(`if "legacy" not specified in automatic mode`, () => { + onRenderBody(ssrArgs, { + icon: true, + theme_color: `#000000`, + include_favicon: false, + cache_busting_mode: `none`, + theme_color_in_head: false, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`if "legacy" not specified in hybrid mode.`, () => { + onRenderBody(ssrArgs, { + icon: true, + theme_color: `#000000`, + icons: [ + { + src: `/favicons/android-chrome-48x48.png`, + sizes: `48x48`, + type: `image/png`, + }, + { + src: `/favicons/android-chrome-512x512.png`, + sizes: `512x512`, + type: `image/png`, + }, + ], + include_favicon: false, + cache_busting_mode: `none`, + theme_color_in_head: false, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`if "legacy" not specified in manual mode.`, () => { + onRenderBody(ssrArgs, { + icon: false, + icons: [ + { + src: `/favicons/android-chrome-48x48.png`, + sizes: `48x48`, + type: `image/png`, + }, + { + src: `/favicons/android-chrome-512x512.png`, + sizes: `512x512`, + type: `image/png`, + }, + ], + }) + expect(headComponents).toMatchSnapshot() + }) + }) + + describe(`Does not create legacy links`, () => { + it(`If "legacy" options is false and in automatic`, () => { + onRenderBody(ssrArgs, { + icon: true, + legacy: false, + include_favicon: false, + cache_busting_mode: false, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`If "legacy" options is false and in manual mode`, () => { + onRenderBody(ssrArgs, { + icon: false, + theme_color: `#000000`, + legacy: false, + icons: [ + { + src: `/favicons/android-chrome-48x48.png`, + sizes: `48x48`, + type: `image/png`, + }, + { + src: `/favicons/android-chrome-512x512.png`, + sizes: `512x512`, + type: `image/png`, + }, + ], + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`If "legacy" options is false and in hybrid mode`, () => { + onRenderBody(ssrArgs, { + icon: true, + legacy: false, + icons: [ + { + src: `/favicons/android-chrome-48x48.png`, + sizes: `48x48`, + type: `image/png`, + }, + { + src: `/favicons/android-chrome-512x512.png`, + sizes: `512x512`, + type: `image/png`, + }, + ], + include_favicon: false, + cache_busting_mode: false, + }) + expect(headComponents).toMatchSnapshot() + }) + }) + }) + + describe(`Cache Busting`, () => { + it(`doesn't add cache busting in manual mode`, () => { + onRenderBody(ssrArgs, { + icon: false, + icons: [ + { + src: `/favicons/android-chrome-48x48.png`, + sizes: `48x48`, + type: `image/png`, + }, + { + src: `/favicons/android-chrome-512x512.png`, + sizes: `512x512`, + type: `image/png`, + }, + ], + cache_busting_mode: `name`, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`doesn't add cache busting if "cache_busting_mode" option is set to none`, () => { + onRenderBody(ssrArgs, { icon: true, cache_busting_mode: `none` }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Does file name cache busting if "cache_busting_mode" option is set to name`, () => { + onRenderBody(ssrArgs, { icon: true, cache_busting_mode: `name` }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Does query cache busting if "cache_busting_mode" option is set to query`, () => { + onRenderBody(ssrArgs, { icon: true, cache_busting_mode: `query` }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Does query cache busting if "cache_busting_mode" option is set to undefined`, () => { + onRenderBody(ssrArgs, { icon: true }) + expect(headComponents).toMatchSnapshot() + }) + }) + + describe(`Favicon`, () => { + it(`Adds link favicon tag if "include_favicon" is set to true`, () => { + onRenderBody(ssrArgs, { + icon: true, + include_favicon: true, + legacy: false, + cache_busting_mode: `none`, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Does not add a link favicon if "include_favicon" option is set to false`, () => { + onRenderBody(ssrArgs, { + icon: true, + include_favicon: false, + legacy: false, + cache_busting_mode: `none`, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Does not add a link favicon if in manual mode`, () => { + onRenderBody(ssrArgs, { + icon: false, + icons: [ + { + src: `/favicons/android-chrome-48x48.png`, + sizes: `48x48`, + type: `image/png`, + }, + { + src: `/favicons/android-chrome-512x512.png`, + sizes: `512x512`, + type: `image/png`, + }, + ], + legacy: false, + cache_busting_mode: `none`, + }) + expect(headComponents).toMatchSnapshot() + }) + }) + + describe(`CORS Generation`, () => { + it(`Adds a crossOrigin attribute to manifest link tag if provided`, () => { + onRenderBody(ssrArgs, { + crossOrigin: `use-credentials`, + legacy: false, + include_favicon: false, + cache_busting_mode: `none`, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Add crossOrigin when 'crossOrigin' is anonymous`, () => { + onRenderBody(ssrArgs, { + icon: true, + crossOrigin: `anonymous`, + legacy: false, + include_favicon: false, + cache_busting_mode: `none`, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Does not add crossOrigin when 'crossOrigin' is blank`, () => { + onRenderBody(ssrArgs, { + icon: true, + legacy: false, + include_favicon: false, + cache_busting_mode: `none`, + }) + expect(headComponents).toMatchSnapshot() + }) + }) +}) diff --git a/packages/gatsby-plugin-manifest/src/__tests__/images/gatsby-logo.png b/packages/gatsby-plugin-manifest/src/__tests__/images/gatsby-logo.png new file mode 100644 index 0000000..71ed7e4 Binary files /dev/null and b/packages/gatsby-plugin-manifest/src/__tests__/images/gatsby-logo.png differ diff --git a/packages/gatsby-plugin-manifest/src/common.js b/packages/gatsby-plugin-manifest/src/common.js new file mode 100644 index 0000000..8b05a07 --- /dev/null +++ b/packages/gatsby-plugin-manifest/src/common.js @@ -0,0 +1,81 @@ +import fs from "fs" +import sysPath from "path" + +// default icons for generating icons +exports.defaultIcons = [ + { + src: `icons/icon-48x48.png`, + sizes: `48x48`, + type: `image/png`, + }, + { + src: `icons/icon-72x72.png`, + sizes: `72x72`, + type: `image/png`, + }, + { + src: `icons/icon-96x96.png`, + sizes: `96x96`, + type: `image/png`, + }, + { + src: `icons/icon-144x144.png`, + sizes: `144x144`, + type: `image/png`, + }, + { + src: `icons/icon-192x192.png`, + sizes: `192x192`, + type: `image/png`, + }, + { + src: `icons/icon-256x256.png`, + sizes: `256x256`, + type: `image/png`, + }, + { + src: `icons/icon-384x384.png`, + sizes: `384x384`, + type: `image/png`, + }, + { + src: `icons/icon-512x512.png`, + sizes: `512x512`, + type: `image/png`, + }, +] + +/** + * Check if the icon exists on the filesystem + * + * @param {String} srcIcon Path of the icon + */ +exports.doesIconExist = function doesIconExist(srcIcon) { + try { + return fs.statSync(srcIcon).isFile() + } catch (e) { + if (e.code !== `ENOENT`) { + throw e + } + + return false + } +} + +/** + * @param {string} path The generic path to an icon + * @param {string} digest The digest of the icon provided in the plugin's options. + */ +exports.addDigestToPath = function(path, digest, method) { + if (method === `name`) { + const parsedPath = sysPath.parse(path) + + return `${parsedPath.dir}/${parsedPath.name}-${digest}${parsedPath.ext}` + } + + if (method === `query`) { + return `${path}?v=${digest}` + } + + return path +} diff --git a/packages/gatsby-plugin-manifest/src/gatsby-node.js b/packages/gatsby-plugin-manifest/src/gatsby-node.js new file mode 100644 index 0000000..83d1438 --- /dev/null +++ b/packages/gatsby-plugin-manifest/src/gatsby-node.js @@ -0,0 +1,150 @@ +import fs from "fs" +import path from "path" +import sharp from "sharp" +import createContentDigest from "gatsby/dist/utils/create-content-digest" +import { defaultIcons, doesIconExist, addDigestToPath } from "./common" + +sharp.simd(true) + +try { + // Handle Sharp's concurrency based on the Gatsby CPU count + // See: http://sharp.pixelplumbing.com/en/stable/api-utility/#concurrency + // See: https://www.gatsbyjs.org/docs/multi-core-builds/ + const cpuCoreCount = require(`gatsby/dist/utils/cpu-core-count`) + sharp.concurrency(cpuCoreCount()) +} catch { + // if above throws error this probably means that used Gatsby version + // doesn't support cpu-core-count utility. +} + +function generateIcons(icons, srcIcon) { + return Promise.all( + icons.map(async icon => { + const size = parseInt( + icon.sizes.substring(0, icon.sizes.lastIndexOf(`x`)) + ) + const imgPath = path.join(`public`, icon.src) + + // For vector graphics, instruct sharp to use a pixel density + // suitable for the resolution we're rasterizing to. + // For pixel graphics sources this has no effect. + // Sharp accept density from 1 to 2400 + const density = Math.min(2400, Math.max(1, size)) + + return sharp(srcIcon, { density }) + .resize({ + width: size, + height: size, + fit: `contain`, + background: { r: 255, g: 255, b: 255, alpha: 0 }, + }) + .toFile(imgPath) + }) + ) +} + +exports.onPostBootstrap = async ({ reporter }, { manifests, ...manifest }) => { + const activity = reporter.activityTimer(`Build manifest and related icons`) + activity.start() + + if (Array.isArray(manifests)) { + await Promise.all(manifests.map(x => makeManifest(reporter, x))) + } else { + await makeManifest(reporter, manifest) + } + + activity.end() +} + +const makeManifest = async (reporter, pluginOptions) => { + const { icon, language, ...manifest } = pluginOptions + const suffix = language ? `_${language}` : `` + + // Delete options we won't pass to the manifest.webmanifest. + delete manifest.plugins + delete manifest.legacy + delete manifest.theme_color_in_head + delete manifest.cache_busting_mode + delete manifest.crossOrigin + delete manifest.icon_options + delete manifest.include_favicon + delete manifest.regex + + // If icons are not manually defined, use the default icon set. + if (!manifest.icons) { + manifest.icons = defaultIcons + } + + // Specify extra options for each icon (if requested). + if (pluginOptions.icon_options) { + manifest.icons = manifest.icons.map(icon => { + return { + ...pluginOptions.icon_options, + ...icon, + } + }) + } + + // Determine destination path for icons. + let paths = {} + manifest.icons.forEach(icon => { + const iconPath = path.join(`public`, path.dirname(icon.src)) + if (!paths[iconPath]) { + const exists = fs.existsSync(iconPath) + //create destination directory if it doesn't exist + if (!exists) { + fs.mkdirSync(iconPath) + } + paths[iconPath] = true + } + }) + + // Only auto-generate icons if a src icon is defined. + if (icon !== undefined) { + // Check if the icon exists + if (!doesIconExist(icon)) { + throw `icon (${icon}) does not exist as defined in gatsby-config.js. Make sure the file exists relative to the root of the site.` + } + + const sharpIcon = sharp(icon) + + const metadata = await sharpIcon.metadata() + + if (metadata.width !== metadata.height) { + reporter.warn( + `The icon(${icon}) you provided to 'gatsby-plugin-manifest' is not square.\n` + + `The icons we generate will be square and for the best results we recommend you provide a square icon.\n` + ) + } + + //add cache busting + const cacheMode = + typeof pluginOptions.cache_busting_mode !== `undefined` + ? pluginOptions.cache_busting_mode + : `query` + + //if cacheBusting is being done via url query icons must be generated before cache busting runs + if (cacheMode === `query`) { + await generateIcons(manifest.icons, icon) + } + + if (cacheMode !== `none`) { + const iconDigest = createContentDigest(fs.readFileSync(icon)) + + manifest.icons.forEach(icon => { + icon.src = addDigestToPath(icon.src, iconDigest, cacheMode) + }) + } + + //if file names are being modified by cacheBusting icons must be generated after cache busting runs + if (cacheMode !== `query`) { + await generateIcons(manifest.icons, icon) + } + } + + //Write manifest + fs.writeFileSync( + path.join(`public`, `manifest${suffix}.webmanifest`), + JSON.stringify(manifest) + ) +} diff --git a/packages/gatsby-plugin-manifest/src/gatsby-ssr.js b/packages/gatsby-plugin-manifest/src/gatsby-ssr.js new file mode 100644 index 0000000..21aba77 --- /dev/null +++ b/packages/gatsby-plugin-manifest/src/gatsby-ssr.js @@ -0,0 +1,109 @@ +import React from "react" +import { withPrefix } from "gatsby" +import createContentDigest from "gatsby/dist/utils/create-content-digest" +import { defaultIcons, addDigestToPath } from "./common.js" +import fs from "fs" + +let iconDigest = null + +exports.onRenderBody = ( + { setHeadComponents, pathname = `/` }, + pluginOptions +) => { + if (Array.isArray(pluginOptions.manifests)) { + pluginOptions = pluginOptions.manifests.find(x => + RegExp(x.regex || `^/${x.language}/.*`).test(pathname) + ) + if (!pluginOptions) return false + } + + // We use this to build a final array to pass as the argument to setHeadComponents at the end of onRenderBody. + let headComponents = [] + + const srcIconExists = !!pluginOptions.icon + + const icons = pluginOptions.icons || defaultIcons + const legacy = + typeof pluginOptions.legacy !== `undefined` ? pluginOptions.legacy : true + + const cacheBusting = + typeof pluginOptions.cache_busting_mode !== `undefined` + ? pluginOptions.cache_busting_mode + : `query` + + // 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)) + } + + const insertFaviconLinkTag = + typeof pluginOptions.include_favicon !== `undefined` + ? pluginOptions.include_favicon + : true + + if (favicon && insertFaviconLinkTag) { + headComponents.push( + + ) + } + } + + const suffix = pluginOptions.language ? `_${pluginOptions.language}` : `` + + // Add manifest link tag. + headComponents.push( + + ) + + // The user has an option to opt out of the theme_color meta tag being inserted into the head. + if (pluginOptions.theme_color) { + const insertMetaTag = + typeof pluginOptions.theme_color_in_head !== `undefined` + ? pluginOptions.theme_color_in_head + : true + + if (insertMetaTag) { + headComponents.push( + + ) + } + } + + if (legacy) { + const iconLinkTags = icons.map(icon => ( + + )) + + headComponents = [...headComponents, ...iconLinkTags] + } + + setHeadComponents(headComponents) + return true +}