Skip to content

Commit 4f91ca0

Browse files
feat: gatsby image placeholder support (#183)
* feat: Add placeholder support for images (#173) * fix: return null for non images * fix merge conflict Co-authored-by: João Pedro Schmitz <oi@joaopedro.cc>
1 parent 90288b8 commit 4f91ca0

File tree

12 files changed

+3949
-2545
lines changed

12 files changed

+3949
-2545
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ yarn-error.log*
1212

1313
# Output
1414
gatsby-source-graphcms/gatsby-*
15+
gatsby-source-graphcms/util/
1516
!gatsby-source-graphcms/src/**

demo/gatsby-config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ require('dotenv').config()
22

33
module.exports = {
44
plugins: [
5-
`gatsby-plugin-image`,
5+
'gatsby-plugin-image',
6+
'gatsby-plugin-sharp',
67
'gatsby-plugin-mdx',
78
'gatsby-plugin-postcss',
89
{

demo/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
"dependencies": {
1212
"@mdx-js/mdx": "1.6.22",
1313
"@mdx-js/react": "1.6.22",
14-
"gatsby": "3.1.0",
14+
"gatsby": "3.1.2",
1515
"gatsby-plugin-image": "1.1.0",
1616
"gatsby-plugin-mdx": "2.1.0",
1717
"gatsby-plugin-postcss": "4.1.0",
18+
"gatsby-plugin-sharp": "3.1.2",
1819
"gatsby-source-graphcms": "2.0.0",
1920
"react": "17.0.1",
2021
"react-dom": "17.0.1"

demo/src/pages/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const query = graphql`
3939
formattedPrice
4040
id
4141
images {
42-
gatsbyImageData(layout: FULL_WIDTH)
42+
gatsbyImageData(layout: FULL_WIDTH, placeholder: BLURRED)
4343
}
4444
locale
4545
name

gatsby-source-graphcms/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ Use the `gatsbyImageData` resolver on your `GraphCMS_Asset` nodes.
190190
| `quality` | Int | The default image quality generated. This is overridden by any format-specific options. |
191191
| `sizes` | String | [The `<img> sizes` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attributes), passed to the img tag. This describes the display size of the image, and does not affect generated images. You are only likely to need to change this if your are using full width images that do not span the full width of the screen. |
192192
| `width` | Int | Change the size of the image. |
193+
| `placeholder` | `NONE`/`BLURRED`/`DOMINANT_COLOR`/`TRACED_SVG` | Choose the style of temporary image shown while the full image loads. |
194+
195+
**NOTE**: `gatsby-plugin-sharp` needs to be listed as a dependency on your project if you plan to use placeholder `TRACED_SVG` or `DOMINANT_COLOR`.
193196

194197
For more information on using `gatsby-plugin-image`, please see the [documentation](https://www.gatsbyjs.com/plugins/gatsby-plugin-image/).
195198

gatsby-source-graphcms/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"license": "MIT",
1818
"dependencies": {
1919
"@babel/runtime": "7.12.5",
20+
"gatsby-core-utils": "2.1.0",
2021
"gatsby-graphql-source-toolkit": "0.6.3",
2122
"gatsby-source-filesystem": "2.11.1",
2223
"he": "1.2.0",

gatsby-source-graphcms/src/gatsby-node.js

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ import {
1010
sourceAllNodes,
1111
sourceNodeChanges,
1212
} from 'gatsby-graphql-source-toolkit'
13-
import { generateImageData } from 'gatsby-plugin-image'
13+
import {
14+
generateImageData,
15+
getLowResolutionImageURL,
16+
} from 'gatsby-plugin-image'
1417
import { getGatsbyImageResolver } from 'gatsby-plugin-image/graphql-utils'
1518
import { createRemoteFileNode } from 'gatsby-source-filesystem'
1619
import he from 'he'
1720
import fetch from 'node-fetch'
1821

22+
import { PLUGIN_NAME } from './util/constants'
23+
import { getImageBase64, getBase64DataURI } from './util/getImageBase64'
24+
import { getImageDominantColor } from './util/getDominantColor'
25+
import { getTracedSVG } from './util/getTracedSVG'
26+
1927
export function pluginOptionsSchema({ Joi }) {
2028
return Joi.object({
2129
buildMarkdownNodes: Joi.boolean()
@@ -81,7 +89,7 @@ const createSourcingConfig = async (
8189
.then((response) => {
8290
if (!response.ok) {
8391
return reporter.panic(
84-
`gatsby-source-graphcms: Problem building GraphCMS nodes`,
92+
`[${PLUGIN_NAME}]: Problem building GraphCMS nodes`,
8593
new Error(response.statusText)
8694
)
8795
}
@@ -91,7 +99,7 @@ const createSourcingConfig = async (
9199
.then((response) => {
92100
if (response.errors) {
93101
return reporter.panic(
94-
`gatsby-source-graphcms: Problem building GraphCMS nodes`,
102+
`[${PLUGIN_NAME}]: Problem building GraphCMS nodes`,
95103
new Error(response.errors)
96104
)
97105
}
@@ -100,7 +108,7 @@ const createSourcingConfig = async (
100108
})
101109
.catch((error) => {
102110
return reporter.panic(
103-
`gatsby-source-graphcms: Problem building GraphCMS nodes`,
111+
`[${PLUGIN_NAME}]: Problem building GraphCMS nodes`,
104112
new Error(error)
105113
)
106114
})
@@ -233,7 +241,7 @@ export async function sourceNodes(gatsbyApi, pluginOptions) {
233241
}
234242

235243
export async function onCreateNode(
236-
{ node, actions: { createNode }, createNodeId, getCache },
244+
{ node, actions: { createNode }, createNodeId, getCache, cache },
237245
{
238246
buildMarkdownNodes = false,
239247
downloadLocalImages = false,
@@ -251,13 +259,14 @@ export async function onCreateNode(
251259
parentNodeId: node.id,
252260
createNode,
253261
createNodeId,
262+
cache,
254263
getCache,
255264
...(node.fileName && { name: node.fileName }),
256265
})
257266

258267
if (fileNode) node.localFile = fileNode.id
259268
} catch (e) {
260-
console.error('gatsby-source-graphcms:', e)
269+
console.error(`[${PLUGIN_NAME}]`, e)
261270
}
262271
}
263272

@@ -334,37 +343,84 @@ const generateImageSource = (
334343
return { src, width, height, format }
335344
}
336345

337-
const resolveGatsbyImageData = async (
338-
{ handle: filename, height, mimeType, width },
339-
options
340-
) => {
341-
const imageDataArgs = {
342-
...options,
343-
pluginName: `gatsby-source-graphcms`,
344-
sourceMetadata: { format: mimeType.split('/')[1], height, width },
345-
filename,
346-
generateImageSource,
347-
options,
348-
}
346+
function makeResolveGatsbyImageData(cache) {
347+
return async function resolveGatsbyImageData(
348+
{ handle: filename, height, mimeType, width, url, internal },
349+
options
350+
) {
351+
if (!mimeType.includes('image/')) {
352+
return null
353+
}
349354

350-
return generateImageData(imageDataArgs)
355+
const imageDataArgs = {
356+
...options,
357+
pluginName: PLUGIN_NAME,
358+
sourceMetadata: { format: mimeType.split('/')[1], height, width },
359+
filename,
360+
generateImageSource,
361+
options,
362+
}
363+
364+
if (options?.placeholder === `BLURRED`) {
365+
const lowResImageURL = getLowResolutionImageURL(imageDataArgs)
366+
367+
const imageBase64 = await getImageBase64({
368+
url: lowResImageURL,
369+
cache,
370+
})
371+
372+
imageDataArgs.placeholderURL = getBase64DataURI({
373+
imageBase64,
374+
})
375+
}
376+
377+
if (options?.placeholder === `DOMINANT_COLOR`) {
378+
const lowResImageURL = getLowResolutionImageURL(imageDataArgs)
379+
380+
imageDataArgs.backgroundColor = await getImageDominantColor({
381+
url: lowResImageURL,
382+
cache,
383+
})
384+
}
385+
386+
if (options?.placeholder === `TRACED_SVG`) {
387+
imageDataArgs.placeholderURL = await getTracedSVG({
388+
url,
389+
internal,
390+
filename,
391+
cache,
392+
})
393+
}
394+
395+
return generateImageData(imageDataArgs)
396+
}
351397
}
352398

353399
export function createResolvers(
354-
{ createResolvers },
400+
{ createResolvers, cache },
355401
{ typePrefix = 'GraphCMS_' }
356402
) {
357403
const typeName = `${typePrefix}Asset`
358404

359405
createResolvers({
360406
[typeName]: {
361407
gatsbyImageData: {
362-
...getGatsbyImageResolver(resolveGatsbyImageData, {
408+
...getGatsbyImageResolver(makeResolveGatsbyImageData(cache), {
363409
quality: {
364410
type: 'Int',
365411
description:
366412
'The default image quality generated. This is overridden by any format-specific options.',
367413
},
414+
placeholder: {
415+
type:
416+
'enum GraphCMSImagePlaceholder { NONE, BLURRED, DOMINANT_COLOR, TRACED_SVG }',
417+
description: `The style of temporary image shown while the full image loads.
418+
BLURRED: generates a very low-resolution version of the image and displays it as a blurred background (default).
419+
DOMINANT_COLOR: the dominant color of the image used as a solid background color.
420+
TRACED_SVG: generates a simplified, flat SVG version of the source image, which it displays as a placeholder.
421+
NONE: No placeholder. Use the backgroundColor option to set a static background if you wish.
422+
`,
423+
},
368424
}),
369425
type: 'JSON',
370426
},
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const PLUGIN_NAME = `gatsby-source-graphcms`
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { fetchRemoteFile } from 'gatsby-core-utils'
2+
3+
import { PLUGIN_NAME } from './constants'
4+
5+
export async function getImageDominantColor({ url, cache }) {
6+
try {
7+
const { getDominantColor } = require(`gatsby-plugin-sharp`)
8+
9+
const filePath = await fetchRemoteFile({
10+
url,
11+
cache,
12+
})
13+
14+
const backgroundColor = await getDominantColor(filePath)
15+
16+
return backgroundColor
17+
} catch {
18+
console.error(
19+
`[${PLUGIN_NAME}] In order to use the dominant color placeholder, you need to install gatsby-plugin-sharp`
20+
)
21+
22+
return `rgba(0, 0, 0, 0.5)`
23+
}
24+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { fetchRemoteFile } from 'gatsby-core-utils'
2+
import { readFileSync } from 'fs'
3+
4+
export function getBase64DataURI({ imageBase64 }) {
5+
return `data:image/png;base64,${imageBase64}`
6+
}
7+
8+
export async function getImageBase64({ url, cache }) {
9+
const filePath = await fetchRemoteFile({
10+
url,
11+
cache,
12+
})
13+
14+
const buffer = readFileSync(filePath)
15+
return buffer.toString(`base64`)
16+
}

0 commit comments

Comments
 (0)