How to use Gatsby Image API with markdown and static files.
2020-01-21
Some notes about working with Images in Markdown Posts and JSX Pages. Belongs to Challenge 3 - Auto-Optimize Images on Your Gatsby Site.
Third Challenge on #100DaysOfGatsby Challenge is about performance: Auto-Optimize Images on Your Gatsby Site.
You can just follow Gatsby documentation, but I will write some notes just to myself.
Before this point we have a blog based in markdown files. In the Markdown Frontmatter we set featured images, in this same MD page we have:
1--- 2title: How to use Gatsby Image API with markdown files. 3subtitle: Some notes about working with Images in Markdown Posts and Pages 4date: 2020-01-21 5update: 2020-01-21 6banner: ../../images/blog/100-days-of-gatsby-challenge.png 7tags: ['javascript', '#100DaysOfGatsby', 'gatsby', 'performance', 'audit', 'SEO'] 8---
We just get all frontmatter fields in a GraphQL query and we print in the JSX. So, in gatsby-node.js:
1const { createFilePath } = require(`gatsby-source-filesystem`) 2 3exports.onCreateNode = ({ node, getNode, actions }) => { 4 const { createNodeField } = actions 5 if (node.internal.type === `MarkdownRemark`) { 6 const slug = createFilePath({ node, getNode, basePath: `pages` }) 7 createNodeField({ 8 node, 9 name: `slug`, 10 value: slug, 11 }) 12 } 13} 14 15exports.createPages = async ({ graphql, actions }) => { 16 const { createPage } = actions 17 const result = await graphql(` 18 query { 19 allMarkdownRemark( 20 sort: { order: DESC, fields: [frontmatter___date] } 21 limit: 2000 22 ) { 23 edges { 24 node { 25 fields { 26 slug 27 } 28 frontmatter { 29 tags 30 } 31 } 32 } 33 } 34 } 35 `) 36 37 const posts = result.data.allMarkdownRemark.edges 38 39 posts.forEach(({ node }) => { 40 createPage({ 41 path: node.fields.slug, 42 component: path.resolve(`./src/templates/blog-post.js`), 43 context: { 44 // Data passed to context is available 45 // in page queries as GraphQL variables. 46 slug: node.fields.slug, 47 }, 48 }) 49 }) 50 51 const tagResult = await graphql(` 52 { 53 allMarkdownRemark(limit: 2000) { 54 group(field: frontmatter___tags) { 55 fieldValue 56 } 57 } 58 } 59`) 60 61 const tags = tagResult.data.allMarkdownRemark.group 62 63 tags.forEach(tag => { 64 createPage({ 65 path: `/blog/tags/${_.kebabCase(tag.fieldValue)}/`, 66 component: path.resolve(`./src/templates/blog-tags.js`), 67 context: { 68 tag: tag.fieldValue, 69 }, 70 }) 71 }) 72}
And in the blog template, blog-post.js:
1import React from "react" 2import { Link, graphql } from "gatsby" 3import Img from "gatsby-image" 4import kebabCase from "lodash/kebabCase" 5import Layout from "../components/layout" 6import SEO from "../components/seo" 7 8export default ({ data }) => { 9 let post = data.markdownRemark 10 let bannerImgFluid = post.frontmatter.banner.childImageSharp.fluid 11 return ( 12 <Layout> 13 <SEO title={post.frontmatter.title} description={post.frontmatter.subtitle} /> 14 <article className="article"> 15 <h2 className="article__title">{post.frontmatter.title}</h2> 16 <h3 className="article__subtitle">{post.frontmatter.subtitle}</h3> 17 <p className="article__date">{post.frontmatter.date}</p> 18 <p className="article__tags">Tags: {post.frontmatter.tags.map((tag, i) => [ 19 <Link key={tag} to={`/blog/tags/${kebabCase(tag)}/`}> 20 {tag}{i < post.frontmatter.tags.length - 1 ? ', ' : ''} 21 </Link> 22 23 ])} 24 </p> 25 <div className="article__image"><Img className="post__image" fluid={bannerImgFluid} /></div> 26 <div className="article__cont" dangerouslySetInnerHTML={{ __html: post.html }} /> 27 </article> 28 </Layout> 29 ) 30} 31 32export const query = graphql` 33 query($slug: String!) { 34 markdownRemark(fields: { slug: { eq: $slug } }) { 35 html 36 frontmatter { 37 title 38 subtitle 39 date(formatString: "DD MMMM, YYYY") 40 banner { 41 childImageSharp { 42 fluid(maxWidth: 1000) { 43 ...GatsbyImageSharpFluid 44 } 45 } 46 } 47 tags 48 } 49 } 50 } 51`
Ok... Yeah... Fair enough, we are also adding tags here, so to see how I added tags to a blog post based in markdown, visit my previous post: How to make a list of tags in a Gatsby JS blog.
And in the blog/index.js file:
1import React from "react" 2import { Link, graphql } from "gatsby" 3import Img from "gatsby-image" 4import Layout from "../../components/layout" 5import SEO from "../../components/seo" 6 7export default ({ data }) => { 8 return ( 9 <Layout> 10 <SEO title="Blog" description={data.site.siteMetadata.description} /> 11 <h2 className="container__title">Blog posts <em>({data.allMarkdownRemark.totalCount})</em>:</h2> 12 <div className="posts"> 13 {data.allMarkdownRemark.edges.map(({ node }) => ( 14 <div key={node.id} className="post"> 15 <Link to={node.fields.slug} title={node.frontmatter.title}><Img className="post__image" fluid={node.frontmatter.banner.childImageSharp.fluid} /></Link> 16 <h3 className="post__title"><Link to={node.fields.slug} title={node.frontmatter.title}>{node.frontmatter.title}{" "}</Link></h3> 17 <p className="post__date">{node.frontmatter.date}</p> 18 19 <div className="post__excerpt">{node.excerpt}</div> 20 </div> 21 ))} 22 </div> 23 </Layout> 24 ) 25} 26 27export const query = graphql` 28 query { 29 site { 30 siteMetadata { 31 role 32 description 33 } 34 } 35 allMarkdownRemark( 36 sort: { 37 fields: [frontmatter___date] 38 order: DESC 39 } 40 ) 41 { 42 totalCount 43 edges { 44 node { 45 id 46 frontmatter { 47 title 48 subtitle 49 date(formatString: "DD MMMM, YYYY") 50 banner { 51 childImageSharp { 52 fluid(maxWidth: 500) { 53 ...GatsbyImageSharpFluid 54 } 55 } 56 } 57 } 58 fields { 59 slug 60 } 61 excerpt 62 } 63 } 64 } 65 } 66`
At this point we can follow with the scope of this post: Gatsby API images with Frontmatter metadata
We are going to grab the image filename from the image frontmatter field and then we will transform it with gatsby-plugin-sharp in a GraphQL query.
Assuming we have gatsby-image, gatsby-transformer-sharp and gatsby-plugin-sharp node packages already installed, we are going to add into gatsby-config.js file a new gatsby-source-filesystem
entry, so previosly we had:
1[…] 2{ 3 resolve: `gatsby-source-filesystem`, 4 options: { 5 name: `src`, 6 path: `${__dirname}/src/`, 7 }, 8}, 9{ 10 resolve: `gatsby-transformer-remark`, 11 options: { 12 plugins: [ 13 { 14 resolve: `gatsby-remark-prismjs`, 15 options: { 16 classPrefix: "language-", 17 showLineNumbers: true, 18 } 19 } 20 ] 21 } 22}, 23`gatsby-transformer-sharp`, 24`gatsby-plugin-sharp`, 25[…]
Once the new frontmatter remark directory has been added to the gatsby-config file, let's see how to query the featured images.
First at all you can get an error:
1Field "banner" must not have a selection since type "String" has no subfields
Ok, I lost many hours with this. And I can swear to you that this is only a path issue. The path to the image in SRC folder that you set in frontmatter banner is not pointing t a real file. It can be both:
- the path to the image in src folder is not correct,
- the image name and/or extension is not correct, so you are pointing to a unexisting file.
You can be realy sure that you are pointing to a real image, but if you follow steps above, installed npm plugins, set correctly gatsby-config.js and gatsby-node.js files, the problem is just that the line:
1banner: ../../images/blog/great-gatsby.jpg
Is not working nicely due to it's incorrect in one or above points.
Did you know what? I had that problem and I was sure (wronly) that my parth was correct, my file was:
1banner: "/blog/100-days-of-gatsby-challenge.png"
Then I tried:
- banner: /blog/100-days-of-gatsby-challenge.png
- banner: ../blog/100-days-of-gatsby-challenge.png
- banner: ../images/blog/100-days-of-gatsby-challenge.png
And finally I saw my wrong, OMG what the Hell path was I setting up?
My SRC tree is:
1-src/ 2-- pages/ 3----blog/ 4------ *.md 5 6-- images/ 7---- blog/ 8------ *.* // all images used for blogging.
So I was wrong with the path to src real files.
That's all! Stop losing time with programming bugs, it's just a matter of path.
So the unfamous Gatsby / GraphQL error:
Field "banner" must not have a selection since type "String" has no subfields
It's just a slip-up.
Hey! But when I try to render blog index page (/blog/index.js)* I got thiserror as well:
1TypeError: Cannot read property 'childImageSharp' of null
Ok, this is again the same error, one of your .md pages still has a wrong path in the frontmatter 'banner' field. I swear you, check it.
And what about to adding optim images to static pages?
Even easier!
In any static page, for example my about.js was previously:
1import React from "react" 2import { useStaticQuery, Link, graphql } from "gatsby" 3import PropTypes from "prop-types" 4import Layout from "../components/layout" 5import SEO from "../components/seo" 6 7const AboutMe = ({ children }) => { 8 const data = useStaticQuery(graphql` 9 { 10 site { 11 siteMetadata { 12 title 13 role 14 description 15 } 16 } 17 } 18 `) 19 20 return ( 21 <Layout> 22 <SEO title="Alberto Fortes" description={data.site.siteMetadata.description} /> 23 <article className="article"> 24 <h2 className="article__title article__title--remark t-c">I’m Freelance UI Engineer / Front-end developer from 2006.</h2> 25 <div className="article__image"><img src="/images/albertofortes-web-developer.png" alt="" /></div> 26 <div className="article__cont"> 27 <h3 className="article__claim t-c">More than 14 years coding as JavaScript, CSS, HTML, PHP expert that can help you to code the HTML5, CSS3 and JavaScript of your project. I have my own team to help me when the work requires it.</h3> 28 <div className="article__cont__cols t-j"> 29 <p>From 2006 I've been working as freelance front-end developer helping important brands to achieve their projects on time. Working with their own teams or other external agencies to provide expertise view in HTML, CSS, JavaScript, Load optimization, Accessibility, Usability and other related skills.</p> 30 <p>My rate starts at 35€/hour with long time collaborations increasing rate with short term works. Currently I'm working as UI Engineer expert at Avallain, anyway, I'm always open to discussing new opportunities for full time work or freelance clients. Write me a line in the form below, maybe me or my colleagues have some time.</p> 31 <p>I like to do things right, to work with professional people but most important is to be a friendly person, so for me, like good Andalusian spaniard, I like the cordiality and fellowship.</p> 32 <p>My specialty is web design and front-end development, I work with Photoshop, Adobe Illustrator or Sketch App and I turn pixels into semantic HTML, CSS and JavaScript code. Take a look to mu uses page to now more about <Link to="/uses/">what I use for working</Link>.</p> 33 </div> 34 </div> 35 </article> 36 </Layout> 37 ) 38} 39 40AboutMe.propTypes = { 41 children: PropTypes.node.isRequired, 42} 43 44export default AboutMe
So we just have to remove any call to static page by the hook: useStaticQuery
So add the query to the file:
1const data = useStaticQuery(graphql` 2 { 3 site { 4 siteMetadata { 5 title 6 role 7 description 8 } 9 } 10 } 11`)
And then add the Img component:
1import Img from "gatsby-image"
and switch regular HTML img with Img component:
1<Img fluid={data.file.childImageSharp.fluid} alt="Me walking down the beach" />
And tha's all!
Final file would be:
1import React from "react" 2import { useStaticQuery, Link, graphql } from "gatsby" 3import Img from "gatsby-image" 4import PropTypes from "prop-types" 5import Layout from "../components/layout" 6import SEO from "../components/seo" 7 8const AboutMe = ({ children }) => { 9 const data = useStaticQuery(graphql` 10 { 11 file(relativePath: { eq: "images/albertofortes-web-developer.png" }) { 12 childImageSharp { 13 fluid { 14 ...GatsbyImageSharpFluid 15 } 16 } 17 } 18 site { 19 siteMetadata { 20 title 21 role 22 description 23 } 24 } 25 } 26 `) 27 28 return ( 29 <Layout> 30 <SEO title="Alberto Fortes" description={data.site.siteMetadata.description} /> 31 <article className="article"> 32 <h2 className="article__title article__title--remark t-c">I’m Freelance UI Engineer / Front-end developer from 2006.</h2> 33 <div className="article__image"><Img fluid={data.file.childImageSharp.fluid} alt="Alberto Fortes is a Front-End freelance developer working as contractor." /></div> 34 <div className="article__cont"> 35 <h3 className="article__claim t-c">More than 14 years coding as JavaScript, CSS, HTML, PHP expert that can help you to code the HTML5, CSS3 and JavaScript of your project. I have my own team to help me when the work requires it.</h3> 36 <div className="article__cont__cols t-j"> 37 <p>From 2006 I've been working as freelance front-end developer helping important brands to achieve their projects on time. Working with their own teams or other external agencies to provide expertise view in HTML, CSS, JavaScript, Load optimization, Accessibility, Usability and other related skills.</p> 38 <p>My rate starts at 35€/hour with long time collaborations increasing rate with short term works. Currently I'm working as UI Engineer expert at Avallain, anyway, I'm always open to discussing new opportunities for full time work or freelance clients. Write me a line in the form below, maybe me or my colleagues have some time.</p> 39 <p>I like to do things right, to work with professional people but most important is to be a friendly person, so for me, like good Andalusian spaniard, I like the cordiality and fellowship.</p> 40 <p>My specialty is web design and front-end development, I work with Photoshop, Adobe Illustrator or Sketch App and I turn pixels into semantic HTML, CSS and JavaScript code. Take a look to mu uses page to now more about <Link to="/uses/">what I use for working</Link>.</p> 41 </div> 42 </div> 43 </article> 44 </Layout> 45 ) 46} 47 48AboutMe.propTypes = { 49 children: PropTypes.node.isRequired, 50} 51 52export default AboutMe 53