Skip to content
This repository has been archived by the owner on Dec 27, 2022. It is now read-only.

Add support for images embedded in the markdown content #122

Closed
wants to merge 2 commits into from

Conversation

unagigd
Copy link

@unagigd unagigd commented Apr 10, 2020

This PR includes an idea to solve the gap in image processing. When we use rich content including images those images are not downloaded and not processed by Gatsby during the build. Instead their URLs are untouched and if they are uploaded on the local instance of Strapi (/uploads folder) there is no way to display them on the final static page.

The idea is to:

  • allow configuration to specify if and which fields should be parsed for images
  • extract images paths and create remote file node for each of them
  • put all the newly created nodes as an array to new field (e.g. if the parsed field name is content the images would go to content_images
  • replace original image's paths with newly created file node's base paths (that will allow us to later for e.g. use gatsby-image)

With that all the files will be copied to Gatsby app.

Proposed configuration could look like this:

{
      resolve: 'gatsby-source-strapi',
      options: {
        apiURL: 'http://localhost:1337',
        contentTypes: ['post', 'post-category'],
        queryLimit: 1000,
        markdownImages: {
          typesToParse: {
            post: ['content']  // here we specify that we only want to parse "content" field on "post" content-type
          }
        }
      }
    }

We're currently using this in our project and it works just fine.
In our case we're replacing all the images from the markdown with gatsby-image, like this (not a real code):

<ReactMarkdown
      source={content}
      renderers={{
        image: ({ src, alt }) => {
          const image = getImage(src); //  this helper function will find an image from Gatsby's GraphQL query that has the same `base` property as found image `src`

          return <Img fixed={image.childImageSharp.fixed} alt={alt} />               
        }
      }}
    />

Thought it may be useful.
#114

Signed-off-by: Maciek Sawicki <maciek@decent.com>
@fradav
Copy link

fradav commented Apr 13, 2020

Thanks it works like you said. With the strapi gatsby tutorial modified chunk codes are :
The getImage is just an array find.

gatsby-config.js

    {
      resolve: "gatsby-source-strapi",
      options: {
        apiURL: process.env.API_URL || "http://localhost:1337",
        contentTypes: [
          // List of the Content Types you want to be able to request from Gatsby.
          "article",
          "category",
        ],
        markdownImages: {
          typesToParse: {
            article: ['content']
          }
        },
        queryLimit: 1000,
      },
    },

src/templates/articles.js

import Img from "gatsby-image"

export const query = graphql`
  query ArticleQuery($id: Int!) {
    strapiArticle(strapiId: { eq: $id }) {
      strapiId
      title
      content
      content_images {
        childImageSharp {
          original {
            src
          }
          fluid {
            ...GatsbyImageSharpFluid
          }
        }
        base
      }
      published_at
      image {
        publicURL
      }
    }
  }
`
            <ReactMarkdown source={article.content}
              renderers={{
                image: ({ src, alt }) => {
                  const image = article.content_images.find(element => element.base === src ); 
                  console.log(image)
                  return <Img fluid={image.childImageSharp.fluid} alt={alt} />
                }
              }}
            />

Thanks again, that's really nice to have gatsby processing inlined image from the strapi editor. Let's hope it will be merged!

@giacomocerquone
Copy link

Awesome

@fradav
Copy link

fradav commented Apr 14, 2020

@unagigd just a quick question, do you know by chance why the Babel compilation step has to be done manually when I import your fork in Gatsby?

@unagigd
Copy link
Author

unagigd commented Apr 14, 2020

@unagigd just a quick question, do you know by chance why the Babel compilation step has to be done manually when I import your fork in Gatsby?

AFAIK Gatsby is not running any scripts inside the local plugins folder. So I assume everything needs to be built in advance and available from the plugin root folder. But I'm fairly new to Gatsby so I can be mistaken.

@amrthdivakr
Copy link

I'm very new to gatsby & strapi, so I don't know if I did it right
I installed this version but can't develop as all the graphql queries gives errors like this

error Cannot query field "allStrapiArticle" on type "Query"

`There was an error in your GraphQL query:
Cannot query field "allStrapiArticle" on type "Query".
If you don't expect "allStrapiArticle" to exist on the type "Query" it is most likely a typo.
However, if you expect "allStrapiArticle" to exist there are a couple of solutions to common problems:

  • If you added a new data source and/or changed something inside gatsby-node.js/gatsby-config.js, please try a restart of your development server
  • The field might be accessible in another subfield, please try your query in GraphiQL and use the GraphiQL explorer to see which fields you can query and what shape they have
  • You want to optionally use your field "allStrapiArticle" and right now it is not used anywhere. Therefore Gatsby can't infer the type and add it to the GraphQL schema. A quick fix is to add a least one entry with that field ("dummy content")
    It is recommended to explicitly type your GraphQL schema if you want to use optional fields. This way you don't have to add the mentioned "dummy content". Visit our docs to learn how you can define the schema for "Query":
    https://www.gatsbyjs.org/docs/schema-customization/#creating-type-definitions
    File: src\pages\index.js:14:11`

and when I change it back to strapi version it works fine.

@ladyofcode
Copy link

Thanks for this! I was being asked to provide this functionality and had no idea where to begin as I'm still learning my way around.

@cyberpok
Copy link

Is this idea going to be merged any time soon? I think this plugin desperately needs this implementation.

@chmielulu
Copy link

We're waiting for merged!

@saksham-kapoor
Copy link

Still waiting :'(

@DerekWW
Copy link

DerekWW commented Aug 14, 2020

Is there any update on when this will get approved/merged? I need this to migrate my content into Strapi. Thanks!

@ghost
Copy link

ghost commented Aug 17, 2020

Any updates on this ? Is it merged ?

@guilty-p01nt3r
Copy link

Here, I'm waiting for merge too.
Thank you for your hard work !

@rburgst
Copy link

rburgst commented Sep 9, 2020

while this is a reasonable approach IMHO it would be better to perform downloading of images and rewriting at the time of the node creation (e.g. check how gatsby-source-wordpress-experimental does it).

@rburgst
Copy link

rburgst commented Sep 9, 2020

Another option would be to try to do it similar to here hygraph/gatsby-source-graphcms#44 (comment)
I.e. create a new node for the markdown field which can then be processed by gatsby-transformer-remark. This would allow using all the image rewriting features for the remark transformer.

@joshuadsingh
Copy link

Hi, how are you creating "content_images"?

@vladwulf
Copy link

Could somebody review that?

@alexrintt
Copy link

There is little left to make the first anniversary...

@iangregsondev
Copy link

Looking forward to this, anybody knows if it's planned to be merged soon ?

@SalahAdDin
Copy link

It would be awesome if we also could add metadata like width and height.

@arisros
Copy link

arisros commented Jan 17, 2021

for people who think will do it yourself. same like @unagigd did on this PR, and want to add some fields such as alternative text, caption from that image like I did.

Sorry if it's not polite. I just want to share from my experience, first I force this update with patch-package. but finally I realized, I miss understand what @unagigd to get image which direct download to url image, I want to add more some field like an API /upload/files. so decide to direct add this code to my gatsby-node.js I combine this code with mind.

add package
yarn add node-fetch commonmark

gatsby-node.js

const fetch = require('node-fetch')
const commonmark = require('commonmark')
const reader = new commonmark.Parser()
const { createRemoteFileNode } = require(`gatsby-source-filesystem`)

module.exports.onCreateNode = async ({ node, actions, store, cache, createNodeId, touchNode, createContentDigest }) => {
  const { createNodeField, createNode } = actions

  // in this case Blog Content Type
  if (node.internal.type === "StrapiBlog") {
  
    const parsed = reader.parse(node.content)
    const walker = parsed.walker()
    let event, nd

    while ((event = walker.next())) {
      nd = event.node
      // process image nodes
      if (event.entering && nd.type === 'image') {
        let fileNodeID, fileNodeBase
        const filePathname = nd.destination
        // using filePathname on the cache key for multiple image field
        const mediaDataCacheKey = `strapi-media-content-${filePathname}`
        const cacheMediaData = await cache.get(mediaDataCacheKey)

        // If we have cached media data and it wasn't modified, reuse
        // previously created file node to not try to redownload
        if (cacheMediaData) {
          fileNodeID = cacheMediaData.fileNodeID
          fileNodeBase = cacheMediaData.fileNodeBase
          touchNode({ nodeId: cacheMediaData.fileNodeID })
        }
        const source_url = `${filePathname.startsWith('http') ? '' : process.env.API_URL || 'http://localhost:1339'}${filePathname}`
        if (!fileNodeID) {
          try {
            const fileNode = await createRemoteFileNode({
              url: source_url,
              store,
              cache,
              createNode,
              createNodeId,
            })

            // If we don't have cached data, download the file
            if (fileNode) {
              fileNodeID = fileNode.id
              fileNodeBase = fileNode.base

              await cache.set(mediaDataCacheKey, {
                fileNodeID,
                fileNodeBase,
              })
            }
          } catch (e) {
            console.log(e)
            // Ignore
          }
        }

        if (fileNodeID) {
          // create an array of parsed and downloaded images as a new field
          if (!node['content_images___NODE']) {
            node['content_images___NODE'] = []
          }
          node['content_images___NODE'].push(fileNodeID)

          node['content'] = node['content'].replace(filePathname, fileNodeBase)
        }
        
        // get rest image and make fields
        try {
          const params = `?url=${source_url}`
          const restFile = (await (await fetch(`${process.env.API_URL || 'http://localhost:1339'}/upload/files${params}`)).json())[0]
          const restFileId = createNodeId(restFile.id)

          const restFileWithFileNodeId = {
            ...restFile,
            fileNodeId: fileNodeID, // refer to file id
          }

          // create rest fields node
          const create = await createNode({
            ...restFileWithFileNodeId,
            id: restFileId,
            internal: {
              type: 'RestImagesMarkdown',
              contentDigest: createContentDigest(restFileWithFileNodeId)
            }
          })
          if (restFileId && create) {
            // create an array of parsed and downloaded images as a new field
            if (!node['content_rest_images___NODE']) {
              node['content_rest_images___NODE'] = []
            }
            node['content_rest_images___NODE'].push(restFileId) // nodeId
  
          }
        } catch (error) {
          console.log('getSourceFileMedia', error)
        }
      }
    }
  
  }
};

use Query

export const query = graphql`
  query BlogBySlug {
    strapiBlog {
      id
      slug
      title
      image {
        ...standardImage
      }
      content
      content_images {
        ...standardImage
        base
        id
      }
      content_rest_images {
        id
        fileNodeId
        alternativeText
        caption
      }
    }
  }
`

renderers image

const renderers = {
  image: ({ src }) => {
    const blog = props.data.strapiBlog
    const image = blog.content_images.find(e => e.base === src ); 
    const rest = blog.content_res_images.find(e => e.fileNodeId === image.id)
    return (
      <ImageParagraph>
        <FluidImg
          style={{ width: '100%' }}
          imgStyle={{ objectFit: 'contain' }}
          loading="lazy" image={image} alt={rest.caption}
        />
        <p>source: {rest.caption}</p>
      </ImageParagraph>
    )
  }
}

but thanks for this PR, I learn about gatsby node works. thanks @unagigd ⚡️

@RobinSJames
Copy link

1000 years later:

Resolved conflicts and a merge would be nice 🤷‍♂️ idk

@unagigd unagigd closed this Jan 20, 2021
@unagigd
Copy link
Author

unagigd commented Jan 20, 2021

Hi. I was looking at the state of this PR, but at some point I gave up. I couldn't get any approving review (from people with write or admin permissions), so I couldn't merge it. Since then my project moved from Strapi.
Sorry for the confusion. I thought it would be helpful.

@SalahAdDin
Copy link

SalahAdDin commented Jan 20, 2021

@unagigd moved to where? which tool?

@OddBlueDog
Copy link

Hi. I was looking at the state of this PR, but at some point I gave up. I couldn't get any approving review (from people with write or admin permissions), so I couldn't merge it. Since then my project moved from Strapi.
Sorry for the confusion. I thought it would be helpful.

That is a shame as this seems like a great feature. Are there any other work arounds that people have found?

@OddBlueDog
Copy link

@alexandrebodin What is currently preventing this being merged in?

@RobinSJames
Copy link

@unagigd that's a genuine pity... I hope your project is booming!

@OddBlueDog The only other workaround I can think of that wouldn't be dev intensive is to retrieve the images from a CDN. At least you could then still get the nice features of lazy loading and traced images.

@SamMatthewsIsACommonName

I'm very new to gatsby & strapi, so I don't know if I did it right
I installed this version but can't develop as all the graphql queries gives errors like this

error Cannot query field "allStrapiArticle" on type "Query"

`There was an error in your GraphQL query:
Cannot query field "allStrapiArticle" on type "Query".
If you don't expect "allStrapiArticle" to exist on the type "Query" it is most likely a typo.
However, if you expect "allStrapiArticle" to exist there are a couple of solutions to common problems:

  • If you added a new data source and/or changed something inside gatsby-node.js/gatsby-config.js, please try a restart of your development server
  • The field might be accessible in another subfield, please try your query in GraphiQL and use the GraphiQL explorer to see which fields you can query and what shape they have
  • You want to optionally use your field "allStrapiArticle" and right now it is not used anywhere. Therefore Gatsby can't infer the type and add it to the GraphQL schema. A quick fix is to add a least one entry with that field ("dummy content")
    It is recommended to explicitly type your GraphQL schema if you want to use optional fields. This way you don't have to add the mentioned "dummy content". Visit our docs to learn how you can define the schema for "Query":
    https://www.gatsbyjs.org/docs/schema-customization/#creating-type-definitions
    File: src\pages\index.js:14:11`

and when I change it back to strapi version it works fine.

The same thing happens to me. Does someone have a current fork / branch I could please install this unmerged version from? I was really counting on being able to have images in the markdown :/

@OddBlueDog
Copy link

OddBlueDog commented Feb 9, 2021

I have published this fork to https://www.npmjs.com/package/@oddbluedog/gatsby-source-strapi-support-markdown-images

Remove gatsby-source-strapi from package.json and replace it with: "@oddbluedog/gatsby-source-strapi-support-markdown-images": "^0.0.19",

Also change your gatsby-config to use resolve: "@oddbluedog/gatsby-source-strapi-support-markdown-images",

@joshuadsingh
Copy link

@OddBlueDog I've implemented your published fork on my project. I haven't done this before and I wanted to ask if there are any drawbacks or things to keep in mind when using it. Like if gatsby-source-strapi gets updated, will I not be able to update my project? Thanks.

@OddBlueDog
Copy link

OddBlueDog commented Mar 12, 2021

@OddBlueDog I've implemented your published fork on my project. I haven't done this before and I wanted to ask if there are any drawbacks or things to keep in mind when using it. Like if gatsby-source-strapi gets updated, will I not be able to update my project? Thanks.

The forked version will keep working with strapi, unless they make some kind of breaking change to their graphql implementation which I think is unlikely.

If a new version of gatsby-source-strapi comes out then this fork will also carry on working, but there might be new features in the latest version that aren’t in the fork.

Hopefully they add markdown image support to a future version of the official package and then we can all switch back! :-) I’ve added one another fix to my fork and a couple of package updates.

I do really like strapi but it seems like this project for gatsby has been forgotten a bit currently.

@anikets
Copy link

anikets commented Apr 23, 2021

Solution if you are using react-markdown to convert md to html:

<ReactMarkdown>
  {section.description.replaceAll('/uploads/', 'http://localhost:1339/uploads/')}
</ReactMarkdown>

This might not be good for SEO, but will get the job of showing images done.

@bendeve
Copy link

bendeve commented May 28, 2021

This is a very big issue. We need this feature. How do you write blog's article ? Like text1, photo1, text2, photo2, ... ?
No change to get it ?

@jeffsdata
Copy link

jeffsdata commented Dec 4, 2021

This is a very big issue. We need this feature. How do you write blog's article ? Like text1, photo1, text2, photo2, ... ? No change to get it ?

I ended up using a repeatable component like:

  • title
  • image
  • description

So, in Strapi, each "section" of my blog post can have a title, image and/or rich text block.

image

And in the React side, I loop through all the sections, making sure to check if the block has content in the relevant part: title (h3), image (full width or max-width), description (markdown).

@timbrandin
Copy link

Hi there, sorry for jumping in this thread but I have added support for markdown images in this plugin if you're keen on using them like we are. https://github.com/relate-app/gatsby-source-strapi

@SalahAdDin
Copy link

Hi there, sorry for jumping in this thread but I have added support for markdown images in this plugin if you're keen on using them like we are. https://github.com/relate-app/gatsby-source-strapi

That repository is not available.

@rroslaniec
Copy link

rroslaniec commented Dec 17, 2021

Gatsby in v4 stops allowing to mutate nodes so the solution from @OddBlueDog became invalid.

I made small changes to his code, but I don't have time to prepare a generic plugin :(

Feel free to modify and release:

const remark = require("remark");
const { selectAll } = require("unist-util-select");

exports.onCreateNode = async ({
  node,
  actions,
  store,
  cache,
  createNodeId,
  createContentDigest,
}) => {
  const { createNode, createNodeField } = actions;

  //TODO: handle different types and fields
  if (node.internal.type === "StrapiEntries") {
    const parsed = remark().parse(node.Description);
    const markdownImages = selectAll(`image`, parsed);

    const images = await Promise.all(
      markdownImages.map((image) => {
        const filePathname = image.url;
        const source_url = `${
          filePathname.startsWith("http")
            ? ""
            : process.env.ADMIN_URL || "http://localhost:1339"
        }${filePathname}`;
        return createRemoteFileNode({
          url: source_url,
          parentNodeId: node.id,
          createNode,
          createNodeId,
          cache,
          store,
        });
      })
    );

    const strapiImages = await Promise.all(
      images.map((image) => {
        const params = `?url=${image.url}`;

        return fetch(
          `${
            process.env.ADMIN_URL || "http://localhost:1339"
          }/upload/files${params}`
        )
          .then((r) => r.json())
          .then((resp) => {
            const strapiImage = resp[0];
            const strapiFileId = createNodeId(strapiImage.id);

            const strapiFileWithFileNodeId = {
              ...strapiImage,
              parent: node.id,
              fileId: image.id,
            };
            createNode({
              ...strapiFileWithFileNodeId,
              id: strapiFileId,
              internal: {
                type: "StrapiImagesFromMarkdown",
                contentDigest: createContentDigest(strapiFileWithFileNodeId),
              },
            });
            return strapiFileId;
          });
      })
    );

    createNodeField({
      node,
      name: "Description_strapi_images",
      value: strapiImages,
    });
    return node;
  }
};

exports.createSchemaCustomization = ({ actions }) => {
  //TODO: use schema type builder https://www.gatsbyjs.com/docs/reference/graphql-data-layer/schema-customization/#gatsby-type-builders
  actions.createTypes(`
    type StrapiEntries implements Node {
      Description_strapi_images: [StrapiImagesFromMarkdown] @link(from: "fields.Description_strapi_images")
    }

    type StrapiImagesFromMarkdown implements Node {
      localFile: File @link(from: "fileId")
    }
  `);
};


@timbrandin
Copy link

@SalahAdDin

Hi there, sorry for jumping in this thread but I have added support for markdown images in this plugin if you're keen on using them like we are. https://github.com/relate-app/gatsby-source-strapi

That repository is not available.

It is now, wasn't aware it was private

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

Successfully merging this pull request may close these issues.

None yet