Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rich text images with fluid gatsby-image #70

Closed
codeithuman opened this issue Jan 30, 2019 · 35 comments
Closed

Rich text images with fluid gatsby-image #70

codeithuman opened this issue Jan 30, 2019 · 35 comments

Comments

@codeithuman
Copy link

I'm having trouble figuring out how to get images from a Contentful rich text field working with fluid Gatsby images.

Specifically, what does the GraphQL query need to look like? Do I need to edit my gatsby-node.js allContentfulArticle query or do I need to add a query to my article-template.js file?

Also, do I need to use documentToHtmlString? If so, what element do I need to write a custom renderer for? Will it look similar to the Advanced Example found here?

Thanks for any and all help! Let me know what additional information is needed.

@codeithuman
Copy link
Author

I have images coming in by putting the following into my plugins[] in gatsby-config.js, but it doesn't use gatsby-image and is not what should be done.

// gatsby-config.js
...
    {
      resolve: `@contentful/gatsby-transformer-contentful-richtext`,
      options: {
        renderOptions: {
          renderNode: {
            [BLOCKS.EMBEDDED_ASSET]: node => {
              return `<img src="${node.data.target.fields.file['en-US'].url}" />`
            }
          },
        },
      },
    },
...

@Khaledgarbaya
Copy link
Contributor

hey @codeithuman,
Unfortunately, that's not possible with the plugin, since it's using html instead.
an alternative is to use a richtext-js transformer like this library and use the gatsby Image component

@codeithuman
Copy link
Author

I see, that makes sense, hence the name ‘ToHTML’. I will give the ‘to JSX’ library you linked a try.

Thanks so much for the help and quick reply. I really appreciate your time @Khaledgarbaya.

@brettinternet
Copy link

brettinternet commented Sep 24, 2019

@Khaledgarbaya Can we reopen this as a feature request? It doesn't seem impossible for the body { json } GraphQL query from a content model's rich text to be further transformed within Gatsby's data layer. This isn't a feature to change how it's rendered, rather how the images in the rich text object within the Gatsby data layer are transformed with sharp.

Perhaps this isn't the right repo, but since Contentful is maintaining the Gatsby Source Contentful plugin, I'm not sure the correct place for this feature request.

@jmdmacapagal
Copy link

anyone solved this yet? cant figure how to query the image from the rich text content on contentful
image

Im rendering it like this (dont mind the squiggy lines)
image

@AnalogMemory
Copy link

@jmdmacapagal So the way I got it to work for me was to utilize the option in gatsby-source-contentful to download assets locally
adding downloadLocal: true to your config for the contentful source

When enabled you'll be able to query all the images
via allContentfulAsset { edges{ node { } } }

For my purposes i did a query for the post and allContentfulAsset
Then filtered the json data for all nodes with embedded-asset-block and then used the id to match a node in the images query with the same contentful_id.
Then I could send that to the gatsby-image component.

I don't know if that's the most efficient way to do it but it's currently working :)

@franklintarter
Copy link

franklintarter commented Dec 8, 2019

I got this to work using @AnalogMemory's concept. Here's the code example:

// RichTextRenderer.js
import React from "react";
import { BLOCKS } from "@contentful/rich-text-types";
import { documentToReactComponents } from "@contentful/rich-text-react-renderer";
import Image from "gatsby-image";
import { useContentfulImage } from "../../hooks";

const options = {
  renderNode: {
    [BLOCKS.EMBEDDED_ASSET]: node => {
      const fluid = useContentfulImage(
        node.data.target.fields.file["en-US"].url
      );
      return (
        <Image title={node.data.target.fields.title["en-US"]} fluid={fluid} />
      );
    }
  }
};

export default ({ richTextJson }) =>
  documentToReactComponents(richTextJson, options);

And the hook that connects the nodeId to the fluid image.

// useContentfulImage.js
import { graphql, useStaticQuery } from "gatsby";

export default assetUrl => {
  const { allContentfulAsset } = useStaticQuery(
    graphql`
      query CONTENTFUL_IMAGE_QUERY {
        allContentfulAsset {
          nodes {
            file {
              url
            }
            fluid(maxWidth: 1050, quality: 85) {
              ...GatsbyContentfulFluid_withWebp
            }
          }
        }
      }
    `
  );
  return allContentfulAsset.nodes.find(n => n.file.url === assetUrl).fluid;
};

Then in your blog post or wherever pass the rich text Json into the RichTextRenderer component <RichTextRenderer richTextJson={bodyTest.json} />

@bsgreenb
Copy link

Think this should be re-opened as the feature has not been built

@EliotSlevin
Copy link

I agree, this is such a standard use case I'm very surprised to find out there isn't a recommended approach for doing this. Building a blog in contentful and having images in the content - I actually expected it to just work. @Khaledgarbaya

  const options = {
    renderNode: {
      // eslint-disable-next-line react/display-name
      [BLOCKS.EMBEDDED_ASSET]: (node) => {
        console.log(node)
        const image = node.data.target.fields.file['en-US']
        const width = image.details.image.width
        return <Img width={image.details.image.width} fluid={{
          aspectRatio: width / image.details.image.height,
          src: image.url + '?w=630&q=80',
          srcSet: ` 
              ${image.url}?w=${width / 4}&&q=80 ${width / 4}w,
              ${image.url}?w=${width / 2}&&q=80 ${width / 2}w,
              ${image.url}?w=${width}&&q=80 ${width}w,
              ${image.url}?w=${width * 1.5}&&q=80 ${width * 1.5}w,
              ${image.url}?w=1000&&q=80 1000w,
          `,
          sizes: '(max-width: 630px) 100vw, 630px'
        }} />
      }
    }
  }

Here was my solution, I just mashed the url into the format react-image was expecting from the fragment. No idea if this is better or worse than @AnalogMemory 's solution

@EliotSlevin
Copy link

EliotSlevin commented Jan 22, 2020

I decided fixed images were better for my solution, this is what I ended up using.

  const options = {
    renderNode: {
      // eslint-disable-next-line react/display-name
      [BLOCKS.EMBEDDED_ASSET]: (node) => {
        const image = node.data.target.fields.file['en-US']
        let width = image.details.image.width
        let height = image.details.image.height

        if (width > 630) {
          // That's a bit too wide, lets size her down
          height = (630 / width) * height
          width = 630
        }

        return <Img fixed={{
          width: width,
          height: height,
          src: `${image.url}?w=${width}&h=${height}q=80&fit=fill`,
          srcSet: ` 
              ${image.url}?w=${width}&h=${height}&q=80&fit=fill 1x,
              ${image.url}?w=${Math.round(width * 1.5)}&h=${Math.round(height * 1.5)}&q=80&fit=fill 1.5x,
              ${image.url}?w=${width * 2}&h=${height * 2}&q=80&fit=fill 2x,
              ${image.url}?w=${width * 3}&h=${height * 3}&q=80&fit=fill 3x
          `
        }} />
      }
    }
  }

I've seen similar performance impact to using gatsby-image the regular way. The blog I'm making has a width of 630px, so that's effectively what I'm using as a manual max-width.

@mimbimbo
Copy link

Is there any type of update on this?

@TomPridham
Copy link

we were able to solve this recently. using @AnalogMemory 's solution didn't work for us because it was causing a huge json blob to be included in our bundle. this removes the manual process of creating the urls and doesn't require including all of the contentful assets

/* gatsby-node.js */
const { get } = require('lodash')

const getImagesFromRichText = edge =>
  get(edge, 'node.body.json.content', []).reduce((acc, c) => {
    const url = get(c, 'data.target.fields.file.en.url')
    if (c.nodeType == 'embedded-asset-block' && url) {
      return [...acc, url]
    }
    return acc
  }, [])

const blogPostData = await graphql(`
  query BlogPostData {
    allContentfulBlogPost {
      edges {
        node {
          slug
          body {
            json
          }
        }
      }
    }
  }
`)

const posts = blogPostData.data.allContentfulBlogPost.edges
posts.forEach((post, index) => {
  const images = getImagesFromRichText(post)

  createPage({
    path: `${pathPrefix}${post.node.slug}/`,
    component,
    context: {
      images,
      slug: post.node.slug,
    },
  })
})

exports.createPagesStatefully = async function({ actions }) {
  const { createPage } = actions

  await createBlogs({
    helpers: { createPage },
    path: '/blogs/',
    component: require('src/templates/blog'),
  })
}

/* src/templates/blog.ts */

import get from 'lodash/get'
import { convertRichText } from 'components/Markup'

export const pageQuery = graphql`
  query BlogPostPageQuery($slug: String!, $images: [String!]!) {
    contentfulBlogPost(slug: { eq: $slug }) {
      slug
      body {
        json
      }
    }
    allContentfulAsset(filter: { file: { url: { in: $images } } }) {
      edges {
        node {
          fluid(maxWidth: 700, quality: 85) {
            ...GatsbyContentfulFluid_withWebp
          }
        }
      }
    }
  }
`

const PostPage: React.SFC<PostPageProps> = props => {
  const { data } = props

  const imageNodes = data.allContentfulAsset.edges || []
  const images = imageNodes.map(edge => edge.node.fluid)
  const richText: RichDocument = get(bodyData, 'json')

  return (
    <div>
      {richText &&
        convertRichText({
          richText,
          images,
        })}
    </div>
  )
}

export default PostPage

/* src/components/markup/index.ts */

import { documentToReactComponents } from '@contentful/rich-text-react-renderer'
import get from 'lodash/get'
import Image from 'src/components/Image'

const nodeRenderer = ({ images }) => (
  {
    renderNode: {
      /**
       * Render Images
       */
      [BLOCKS.EMBEDDED_ASSET]: (node: Block) => {
        if (node.data.target) {
          const { title, description, file } = get(
            node,
            'data.target.fields',
            {}
          )
          // image.src has a url param that we need to strip off to match file.url
          <Image
            src={file.url}
            fluid={images.find(
              image => image.src.split('?')[0] === file.url
            )}
          />
        }
      },
      // ...other rendering functions
    },
  }
)

export const convertRichText = ({
  images,
  richText,
}: ConvertRichTextArgs): React.ReactNode =>
  documentToReactComponents(
    richText,
    nodeRenderer({ images })
  )

@daydream05
Copy link

@TomPridham thank you!! This is super helpful and I got it to work!

For anyone looking to do the same, don't just blindly copy paste some of the code like I did.

Don't forget the api returns en-US so you have to use data.target.fields.file[en-US].url instead. I guess just make sure you modify the code to match your data shape.

@bsgreenb
Copy link

bsgreenb commented Apr 8, 2020

Is there an official fix in the works? It's unclear which hacky solution we're supposed to use:

@codeithuman could you re-open?

@richhiggins
Copy link

richhiggins commented Apr 9, 2020

Is there an official fix in the works? It's unclear which hacky solution we're supposed to use:

@codeithuman could you re-open?

We're also using the first approach, it looks like this

[BLOCKS.EMBEDDED_ASSET]: node => {
          return (
            <ContentfulPageImage
              title={node.data.target.fields.description['en-GB']}
              contentfulId={node.data.target.sys.id.replace(/^c(\d)/, '$1')}
            />
          )
        },

Our <ContentfulPageImage /> has a useStaticQuery hook and fetches images (containing fluid fields) using a custom GraphQL resolver - to limit results and so there's less to loop through and smaller bundle impact.

I don't believe a better approach is available right now (?)

[edit] - actually @TomPridham's approach looks good and even better on the bundle!

@bsgreenb
Copy link

Note that @TomPridham 's approach does not cover the recursive case of an entry embed that has an image field you want to use (as opposed to a directly embedded asset).

This deserves its own PR.. Can we please re-open @codeithuman

@Khaledgarbaya
Copy link
Contributor

Hey Folks, Please let not forget that the react renderer needs to work also for plain react app. Also, not everyone is using gatsby-image.

My suggestion is to get the learnings from this issue and create a helper renderer library that support these cases and people can install seperatly.

// PS: this is an imaginary code
import gatsby-renderer-options from 'gatsby-renderer-options'

//......
 documentToReactComponents(
    richText,
   gatsby-renderer-options
  )

@brettinternet
Copy link

@Khaledgarbaya This issue is getting brought up here because the gatsby-source-contentful is in the Gatsby monorepo. Contentful should take more responsibility for that plugin.

@bsgreenb
Copy link

bsgreenb commented Apr 22, 2020

image

Discussion of this issue on Contentful Slack https://contentful-community.slack.com/archives/CBYTK7T9S/p1586228113005000

@RahmiTufanoglu
Copy link

Why is this issue closed? Is there a proper way to handle gatsby-image in rich Text?

@daydream05
Copy link

@Khaledgarbaya would a Contentful gatsby image helper be possible solution?

I guess similar to how sanity-source-plugin.

import Img from 'gatsby-image'
import {getFluidGatsbyImage, getFixedGatsbyImage} from 'gatsby-source-sanity'

const sanityConfig = {projectId: 'abc123', dataset: 'blog'}
const imageAssetId = 'image-488e172a7283400a57e57ffa5762ac3bd837b2ee-4240x2832-jpg'

const fluidProps = getFluidGatsbyImage(imageAssetId, {maxWidth: 1024}, sanityConfig)

<Img fluid={fluidProps} />

This makes it easy to work with gatsby images in Sanity's rich text.
Also would be an opt in thing for folks that only use Gatsby and keeps react renderer for everybody.

@daydream05
Copy link

daydream05 commented May 30, 2020

I think it might work. Exposing resolveFluid would allow us to pass in the asset data from rich text then we'll get that fluidProps back. Will create a prototype this weekend!

@bsgreenb
Copy link

Could we start by re-opening the issue?

@daydream05
Copy link

daydream05 commented May 30, 2020

Alright I got it to work! I think this is the easiest and cleanest way to do it.

So we can essentially use the resolveFluid and resolveFixed inside extendNodetypes. I'll write a PR at gatsby's repo if we can have access to those functions.

So all we would have to do is

// image shape is the same as the one returned by your assets in rich text. (Without the localization)
const fluidProps = resolveFluid(image, {})

But here's the gist for those interested in implementing this themselves while waiting for it to merge.

Edit: fixed the gist link

@bsgreenb
Copy link

bsgreenb commented Jun 1, 2020

Thanks @daydream05 Will check out this approach and report back!

@ssijak
Copy link

ssijak commented Jun 3, 2020

@daydream05 can you please add the gist, the url returns 404

@daydream05
Copy link

@daydream05 can you please add the gist, the url returns 404

Fixed it! Sorry bout that.

Here's the link as well:

https://gist.github.com/daydream05/b5befd50f9c9001fb094f331f98a3ec5

@daniellangnet
Copy link

@daydream05 this is a great solution, thanks so much!

@chang-ryan
Copy link

we should be seeing a fix from the team here: gatsbyjs/gatsby#25249

yahoo!

@Erythros
Copy link

Erythros commented Oct 20, 2020

Since I'm not super advanced, I'm a bit lost here.

What's the proper way to handling this now, given the answer from @chang-ryan ? I am a bit lost following gatsbyjs/gatsby#25249

@daydream05
Copy link

@Erythros the next version of Contentful can handle it but there’s no timeline yet on when it gets merged. But you can use it now and experiment with it. (I use it for production for 3 client sites and they seem to work fine)

npm i gatsby-source-contentful@next

But if you’re stuck with the current version. You can use the solution I linked.

@bsgreenb
Copy link

I highly recommend the next release, which should be merged in due time but is working great for us so far!

@Erythros
Copy link

I highly recommend the next release, which should be merged in due time but is working great for us so far!

Might as well wait for it. Is there any setup or configuration needed or should work out of the box by just doing this?

{documentToReactComponents(element.json)}

@xbaha
Copy link

xbaha commented Sep 9, 2021

is the next version out? and what is the solution?

@codypl
Copy link

codypl commented Nov 7, 2021

Hello !
I have been struggling to get it work so for the people passing by, here is my solution :

My graphql request :

allContentfulBlogPost {
    edges {
        node {
          id
          title
          content {
            raw
            references {
              ... on ContentfulAsset {
                contentful_id
                gatsbyImageData(layout: FULL_WIDTH, quality: 80, formats: [WEBP, AUTO], placeholder: BLURRED)
                description
              }
            }
          }
        }
    }
}

My Post component :

import React from "react"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import { BLOCKS } from '@contentful/rich-text-types'

const richTextImages = {};

const options = {
    renderNode: {
        [BLOCKS.EMBEDDED_ASSET]: node => {
          const imageData = richTextImages[node.data.target.sys.id];
          const image = getImage(imageData.image)
          return <GatsbyImage image={image} alt={imageData.alt}/>
        },
    },
}

const Post = ({ pageContext, location }) => {
  const { post } = pageContext

  post.content.references.map(reference => (
    richTextImages[reference.contentful_id] = {"image": reference.gatsbyImageData, "alt": reference.description}
  ))

  return (
      <>
          <h1>{post.title}</h1>
          <div>{documentToReactComponents(JSON.parse(post.content.raw), options)}</div>
      </>
  )
}

export default Post

I hope this will help !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests