-
-
Save daydream05/b5befd50f9c9001fb094f331f98a3ec5 to your computer and use it in GitHub Desktop.
import _ from 'lodash' | |
import qs from 'qs' | |
// @see https://www.contentful.com/developers/docs/references/images-api/#/reference/resizing-&-cropping/specify-width-&-height | |
const CONTENTFUL_IMAGE_MAX_SIZE = 4000 | |
const isImage = image => | |
_.includes( | |
[`image/jpeg`, `image/jpg`, `image/png`, `image/webp`, `image/gif`], | |
_.get(image, `file.contentType`) | |
) | |
const getBasicImageProps = (image, args) => { | |
let aspectRatio | |
if (args.width && args.height) { | |
aspectRatio = args.width / args.height | |
} else { | |
aspectRatio = | |
image.file.details.image.width / image.file.details.image.height | |
} | |
return { | |
baseUrl: image.file.url, | |
contentType: image.file.contentType, | |
aspectRatio, | |
width: image.file.details.image.width, | |
height: image.file.details.image.height, | |
} | |
} | |
const createUrl = (imgUrl, options = {}) => { | |
// Convert to Contentful names and filter out undefined/null values. | |
const args = _.pickBy( | |
{ | |
w: options.width, | |
h: options.height, | |
fl: options.jpegProgressive ? `progressive` : null, | |
q: options.quality, | |
fm: options.toFormat || ``, | |
fit: options.resizingBehavior || ``, | |
f: options.cropFocus || ``, | |
bg: options.background || ``, | |
}, | |
_.identity | |
) | |
return `${imgUrl}?${qs.stringify(args)}` | |
} | |
export const getFluidGatsbyImage = (image, options) => { | |
if (!isImage(image)) return null | |
console.log(image) | |
const { baseUrl, width, aspectRatio } = getBasicImageProps(image, options) | |
let desiredAspectRatio = aspectRatio | |
// If no dimension is given, set a default maxWidth | |
if (options.maxWidth === undefined && options.maxHeight === undefined) { | |
options.maxWidth = 800 | |
} | |
// If only a maxHeight is given, calculate the maxWidth based on the height and the aspect ratio | |
if (options.maxHeight !== undefined && options.maxWidth === undefined) { | |
options.maxWidth = Math.round(options.maxHeight * desiredAspectRatio) | |
} | |
// If we're cropping, calculate the specified aspect ratio. | |
if (options.maxHeight !== undefined && options.maxWidth !== undefined) { | |
desiredAspectRatio = options.maxWidth / options.maxHeight | |
} | |
// If the users didn't set a default sizes, we'll make one. | |
if (!options.sizes) { | |
options.sizes = `(max-width: ${options.maxWidth}px) 100vw, ${options.maxWidth}px` | |
} | |
// Create sizes (in width) for the image. If the max width of the container | |
// for the rendered markdown file is 800px, the sizes would then be: 200, | |
// 400, 800, 1200, 1600, 2400. | |
// | |
// This is enough sizes to provide close to the optimal image size for every | |
// device size / screen resolution | |
let fluidSizes = [] | |
fluidSizes.push(options.maxWidth / 4) | |
fluidSizes.push(options.maxWidth / 2) | |
fluidSizes.push(options.maxWidth) | |
fluidSizes.push(options.maxWidth * 1.5) | |
fluidSizes.push(options.maxWidth * 2) | |
fluidSizes.push(options.maxWidth * 3) | |
fluidSizes = fluidSizes.map(Math.round) | |
// Filter out sizes larger than the image's maxWidth and the contentful image's max size. | |
const filteredSizes = fluidSizes.filter(size => { | |
const calculatedHeight = Math.round(size / desiredAspectRatio) | |
return ( | |
size <= CONTENTFUL_IMAGE_MAX_SIZE && | |
calculatedHeight <= CONTENTFUL_IMAGE_MAX_SIZE && | |
size <= width | |
) | |
}) | |
// Add the original image (if it isn't already in there) to ensure the largest image possible | |
// is available for small images. | |
if ( | |
!filteredSizes.includes(parseInt(width)) && | |
parseInt(width) < CONTENTFUL_IMAGE_MAX_SIZE && | |
Math.round(width / desiredAspectRatio) < CONTENTFUL_IMAGE_MAX_SIZE | |
) { | |
filteredSizes.push(width) | |
} | |
// Sort sizes for prettiness. | |
const sortedSizes = _.sortBy(filteredSizes) | |
// Create the srcSet. | |
const srcSet = sortedSizes | |
.map(width => { | |
const h = Math.round(width / desiredAspectRatio) | |
return `${createUrl(image.file.url, { | |
...options, | |
width, | |
height: h, | |
})} ${Math.round(width)}w` | |
}) | |
.join(`,\n`) | |
return { | |
aspectRatio: desiredAspectRatio, | |
baseUrl, | |
src: createUrl(baseUrl, { | |
...options, | |
width: options.maxWidth, | |
height: options.maxHeight, | |
}), | |
srcSet, | |
sizes: options.sizes, | |
} | |
} |
Thanks so much for this!
I've got your code plugged in, but I'm still struggling to get any images to render. I must be missing something obvious; I'm still pretty new to Gatsby.
Here are my options within my blog post template with the relevant imports:
import { documentToReactComponents } from '@contentful/rich-text-react-renderer'
import { BLOCKS } from '@contentful/rich-text-types'
import Img from 'gatsby-image'
import { getFluidGatsbyImage } from '../components/getFluidGatsbyImage'
const options = {
[BLOCKS.EMBEDDED_ASSET]: node => {
const { file, title } = data.target.fields
const image = {
file: file['en-US'],
}
const fluidProps = getFluidGatsbyImage(image, { maxWidth: 720 })
return <Img fluid={fluidProps} alt={title['en-US']} />
},
}
Does anything jump out from this snippet?
Here's the full source code of the blog post for more context (forgive my awful code):
https://github.com/Seth-Carter/uk-history-tourist/blob/master/src/templates/blogTemplate.js
@Seth-Carter Ok so I think this is my fault. I forgot to add renderNode on the example.
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ASSET]: node => {
const { file, title } = node.data.target.fields
const image = {
file: file['en-US'],
}
const fluidProps = getFluidGatsbyImage(image, { maxWidth: 720 })
return <Img fluid={fluidProps} alt={title['en-US']} />
},
}
}
Thanks for catching that. I also realized that I wasn't using the 'node' parameter that is passed down through the callback.
It's always the simple mistakes...
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ASSET]: node => {
const { file, title } = node.data.target.fields
const image = {
file: file['en-US'],
}
const fluidProps = getFluidGatsbyImage(image, { maxWidth: 720 })
return <Img fluid={fluidProps} alt={title['en-US']} />
},
}
}
Thanks again for this really handy utility component! This is by the far the best solution I've found for getting Contentful rich text images to render in Gatsby.
@Seth-Carter glad to hear that! There's an upcoming version of the contentful source that will allow us to query for gatsby image and also fixes a lot of rich text problems.
Wow, thanks so much. After trying loads of different solutions, this finally worked for me.
Thank you!
To the folks who are currently using this or about to use it, there's an upcoming update on the gatsby-source-contentful
. See here: gatsbyjs/gatsby#25249
You can do an npm install gatsby-source-contentful@next
if you wanna give it a try.
The new update would allow us to reference images straight to rich text so you won't hopefully have to use this hack. Note though that it has breaking changes.
I've been using it for my new projects and have little issues so far. And the best part is querying reference fields inside rich text. Those were harder to "hack" because we didn't have utility like it similar to gatsby-images.
Thanks for the headsup, I already tried it before using your solution. Sadly when using gatsby-source-contentful@next
I got a couple of errors which I wasn't really in the mood for debugging for a not yet released version.
P.s. do you have an ETA when the update will be shipped?
@daydream05 thanks for keeping this updated. I'm going to try gatsby-source-contentful@next, wading through the errors seems worth it.
@daydream05 any idea why I'm getting a 'Check render method of ...' error? All the options appear to be returning the expected values.
@samh21 can you share the code for the News component? This error happens if your component is not returning anything.
The new version of contentful source is live at [email protected]
!
Thanks for the headsup, do you have a link to the relevant changes to this topic?
Thank you!
Thanks for great solution. Finally solved fluid image issue after trying every solution out there.
@ghoshanjega ignore that! It's a relic from one of the "hacks" that I was using before.