contentful / rich-text

Libraries for handling and rendering Rich Text 📄
MIT License
549 stars 110 forks source link

Rich text images with fluid gatsby-image #70

Closed codeithuman closed 5 years ago

codeithuman commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

@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 commented 4 years ago

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 commented 4 years ago

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

EliotSlevin commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

Is there any type of update on this?

TomPridham commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

image

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

RahmiTufanoglu commented 4 years ago

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

daydream05 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

Could we start by re-opening the issue?

daydream05 commented 4 years ago

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 commented 4 years ago

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

ssijak commented 4 years ago

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

daydream05 commented 4 years ago

@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 commented 4 years ago

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

chang-ryan commented 4 years ago

we should be seeing a fix from the team here: https://github.com/gatsbyjs/gatsby/pull/25249

yahoo!

Erythros commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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

Erythros commented 4 years ago

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 commented 3 years ago

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

codypl commented 3 years ago

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 !