Skip to content

Instantly share code, notes, and snippets.

@daydream05
Created May 30, 2020 03:13
Show Gist options
  • Save daydream05/b5befd50f9c9001fb094f331f98a3ec5 to your computer and use it in GitHub Desktop.
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,
}
}
@daniellangnet
Copy link

Very helpful. Thank you

@daydream05
Copy link
Author

Happy to help! Let me know if it works! I'll add the getBase64Image API on the pull request I created.

@JoseSoteloCohen
Copy link

Awesome! thank you

@ghoshanjega
Copy link

This is awesome.

In your example use case, is "&& images" supposed to be there? And if so what are you referring to?

[BLOCKS.EMBEDDED_ASSET]: (node) => {
        if (node.data.target && images) {
          const { description, file } = get(

@daydream05
Copy link
Author

@ghoshanjega ignore that! It's a relic from one of the "hacks" that I was using before.

@Seth-Carter
Copy link

Seth-Carter commented Aug 29, 2020

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

@daydream05
Copy link
Author

daydream05 commented Aug 30, 2020

@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']} />
       },
    }
  }

@Seth-Carter
Copy link

Seth-Carter commented Aug 30, 2020

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.

@daydream05
Copy link
Author

@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.

@hannessolo
Copy link

Wow, thanks so much. After trying loads of different solutions, this finally worked for me.

@ctekk7
Copy link

ctekk7 commented Oct 7, 2020

Thank you!

@daydream05
Copy link
Author

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.

@ctekk7
Copy link

ctekk7 commented Oct 8, 2020

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?

@bsgreenb
Copy link

@daydream05 thanks for keeping this updated. I'm going to try gatsby-source-contentful@next, wading through the errors seems worth it.

@samh21
Copy link

samh21 commented Nov 10, 2020

@daydream05 any idea why I'm getting a 'Check render method of ...' error? All the options appear to be returning the expected values.

Screenshot 2020-11-10 at 12 45 41

@daydream05
Copy link
Author

@samh21 can you share the code for the News component? This error happens if your component is not returning anything.

@daydream05
Copy link
Author

The new version of contentful source is live at [email protected]!

@extendit-experts
Copy link

extendit-experts commented Nov 14, 2020

Thanks for the headsup, do you have a link to the relevant changes to this topic?

@extendit-experts
Copy link

Thank you!

@yogeshdecodes
Copy link

Thanks for great solution. Finally solved fluid image issue after trying every solution out there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment