How to use GraphQL queries in a container class component - javascript

👋
My current GatsbyJS project is a one pager with a carousel and some other content.
Background
The carousel should be filled with information about some products. My goal is to let the carousel build itself by iterating through all markdown files, picking the ones with these three lines at the top of the file:
---
type: product
---
So I've created a CarouselContainer (class component) and a Carousel component (functional component). The Container should load the markdown via GraphQL query and pass the resulting products object to it's nested component. Then the component should map over the object and create the carousel.
But there are also other markdown files for menu lists, text modals and so on. They have the type: page. I thought preparing a few GraphQL queries would be the solution. But it turned out to be more difficult than expected...
The container component is a class component, so I am not able to call the query directly in it (https://github.com/gatsbyjs/gatsby/issues/3991#issuecomment-364939030).
Then I thought putting multiple queries into the pages/index.js could be the solution.
export const indexQuery = graphql`
query IndexQuery {
allMarkdownRemark(filter: {frontmatter: {type: {eq: "page"}}}) {
edges {
node {
frontmatter {
title
text
}
}
}
}
}
`
export const productsQuery = graphql`
query ProductsQuery {
allMarkdownRemark(filter: {frontmatter: {type: {eq: "product"}}}) {
edges {
node {
id
frontmatter {
title
content
}
}
}
}
}
`
Nope again. Using GraphQL fragments should be a solution...
Q Can someone tell me how to prepare fragments for that purpose and/or have another idea how to get the markdown content right into my container?
Thanks for reading.

You're not too far off. GraphQL supports multiple discrete nodes being queried in the same query:
export const query = graphql`
{
products: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "product" } } }
) {
edges {
# ...
}
}
pages: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "pages" } } }
) {
edges {
# ...
}
}
}
`
Note that I've used aliases to fetch the same initial node (allMarkdownRemark) with separate filters in the same GraphQL query. This will result in data.products and data.pages being passed into your default exported React component.
To clean this up, you can use fragments, allowing you to colocate your products query in your Carousel file:
In carousel.js (or whatever file houses your Carousel component):
export const query = graphql`
fragment Products on Query {
products: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "product" } } }
) {
edges {
# ...
}
}
}
`
Then in your page file:
export const query = graphql`
{
pages: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "pages" } } }
) {
edges {
# ...
}
}
...Products
}
`
Note: if you're using Gatsby 1.x you'll need to change the on Query portion of the fragment to on RootQueryType.
Assuming you're using Gatsby v2, you can also use StaticQuery instead of combining the query into one. This is particularly useful if your pages have no bearing on the Carousel of products.
import React from "react";
import { graphql, StaticQuery } from "gatsby";
class Carousel extends React.Component {
// ...
}
export default props => (
<StaticQuery
query={graphql`
products: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "product" } } }
) {
edges {
# ...
}
}
`}
render={({ products }) => <Carousel products={products} {...props} />}
/>
);

Related

Gatsby Head API not working in production build

I am new to Gatsby. I have almost created the entire application sourcing data from my WordPress blog. Everything is working fine except the head API is not outputting any head tag in production. I am using "Add WPGraphQL SEO" plugin to get Yoast SEO data. The query returns an object named fullHead that contains all the page/post's head data.
Here is the post template code -
import React from "react"
import Layout from "../components/layouts/single-post-layout.js"
import { graphql } from "gatsby"
import HTMLReactParser from "html-react-parser"
import SinglePost from "../components/posts/single-post.js"
export const Head = ({ data }) => (
<>{HTMLReactParser(data.wpPost?.seo?.fullHead)}</>
)
const Post = ({ data }) => {
return (
<Layout post={data}>
<SinglePost post={data.wpPost} />
</Layout>
)
}
export const query = graphql`
query($id: String) {
wpPost(id: { eq: $id }) {
id
categories {
nodes {
id
name
uri
}
}
commentCount
content
date
excerpt
featuredImage {
node {
altText
localFile {
childImageSharp {
gatsbyImageData(
placeholder: BLURRED
formats: [AUTO, WEBP, AVIF]
)
}
}
}
}
slug
uri
title
tags {
nodes {
id
name
uri
}
}
author {
node {
avatar {
url
}
name
description
uri
}
}
comments {
nodes {
author {
node {
avatar {
url
}
name
url
}
}
id
content
date
}
}
seo {
fullHead
title
breadcrumbs {
url
text
}
}
}
}
`
export default Post
The above code works fine in the development build but not in the production build. What am I doing wrong here?

Gatsby Static Image(gatsby-plugin-image) inside MDX

Recently I have started working with Gatsby and right now I'm trying things with MDX, In my MDX file i can work with Gatsby Image throught GraphQL, but I want to use Static Image from gatsby-plugin-image and I'm getting errors like this:
react_devtools_backend.js:2557 Image not loaded
https://images.unsplash.com/photo-1597305877032-0668b3c6413a?w=1300
When i try to implement this image inside .tsx it works so I'm wondering if it is possible or not.
gatsby-config
"gatsby-remark-images",
{
resolve: "gatsby-plugin-mdx",
options: {
defaultLayouts: {
default: require.resolve("./src/components/common/Layout.tsx")
},
gatsbyRemarkPlugins: [
{
resolve: `gatsby-remark-images`,
options: {},
},
],
}
},
{
resolve: "gatsby-source-filesystem",
options: {
name: "images",
path: `${__dirname}/src/images/`,
},
__key: "images",
},
Then in test.mdx I'm trying to use Static Image like this:
<StaticImage
src={'https://images.unsplash.com/photo-1597305877032-0668b3c6413a?w=1300'}
alt={''}
width={3840}
height={1000}
layout={'constrained'}
/>
You can't use gatsby-plugin-image directly in an MDX document. This post on the Gatsby blog explains how to use it indirectly by passing in image reference props via your Frontmatter.
Personally I've been able to do it like so:
This example only loads local images, refer to the blog post for how to reference remote ones as it's more complex.
Template Component
import React from "react";
import { graphql } from "gatsby";
import { MDXRenderer } from "gatsby-plugin-mdx";
import Layout from "../components/layout";
const Game = ({ data }) => {
const { mdx } = data;
const { frontmatter, body } = mdx;
return (
<Layout title={frontmatter.title}>
<span className="date">{frontmatter.date}</span>
<MDXRenderer localImages={frontmatter.embeddedImagesLocal}>
{body}
</MDXRenderer>
</Layout>
);
};
export const pageQuery = graphql`
query($slug: String!) {
mdx(slug: { eq: $slug }) {
slug
body
frontmatter {
date(formatString: "MMMM DD, YYYY")
title
embeddedImagesLocal {
childImageSharp {
gatsbyImageData
}
}
}
}
}
`;
export default Game;
MDX Document
---
title: Death Stranding
author: Hideo Kojima
date: 2021-05-06
template: game
embeddedImagesLocal:
- '../images/20210513035720_1.jpg'
---
import { getImage, GatsbyImage } from 'gatsby-plugin-image';
A great game from Hideo Kojima.
<GatsbyImage alt='Sam in a destroyed mall' image={getImage(props.localImages[0])} />
You can use the gatsby-remark-images plugin with gatsby-plugin-mdx and it handles the images for you.
Install the plugin, then in gatsby-config.js, update the gatsby-plugin-mdx to something like this:
{
resolve: 'gatsby-plugin-mdx',
options: {
gatsbyRemarkPlugins: [
{
resolve: `gatsby-remark-images`,
options: {
maxWidth: 900,
},
},
],
plugins: [`gatsby-remark-images`]
},
},
Then images work as expected using the ![alt](url) markdown format.
Unfortunately the maxWidth is fixed across the site which wasn't ideal for me. I'm using the #bonobolabs/gatsby-remark-images-custom-widths fork which lets you specify the image width in the MDX file with an HTML style img tag:
<img src="./image.jpg" alt="My image" width="500px"/>
I believe it's only 'width' that is the extra property this gives you.

Getting an "Exported queries are only executed for Page components." in Gatsby when trying to generate pages

This seems to be a relatively common problem. I am trying to generate blog post pages but am experiencing this error and the pages show a 404 on load. Which means that they are not being generated.
Here is my code for the gatsby.node.js file:
exports.createPages = async ({ graphql, useStaticQuery, actions: { createPage } }) => {
const postQuery = graphql(`
{
gcms {
posts(where: { stage: PUBLISHED }) {
id
slug
}
}
}
`);
const {
gcms: { posts },
} = useStaticQuery(postQuery);
posts.forEach(({ id, slug }) =>
createPage({
path: `/blog/${slug}`,
component: require.resolve(`./src/templates/PostPage.js`),
context: {
id: id,
slug: slug,
},
})
);
};
And my code for the blog post PostPage.js file:
/* eslint-disable react/prop-types */
import React from 'react';
import { graphql } from 'gatsby';
import Layout from "../components/layout";
//import galaxy from "../images/galaxy.jpg";
import SEO from "../components/seo";
export const postPageQuery = graphql`
query PostPageQuery($id: ID!) {
gcms {
post(where: { id: $id }) {
title
slug
excerpt
postContentMarkdown
tags
author {
name
biography
}
seo {
title
description
keywords
}
}
}
}
`;
const PostPage = ({data: {post}}) => {
return (
<Layout>
<SEO
keywords={[
`ui`,
`ux`,
]}
title="Blog" />
{post.slug}
</Layout>
);
};
export default PostPage;
There are a few things that caught my attention and may fix your issue.
The usage of useStaticQuery in your gatsby-node.js. You don't need to fetch postQuery data with the static query hook since you are using the hook outside a component.
The usage of where filter. According to GraphQL documentation, the way to filter data is by using filter filter. In addition, when filtering the filtered criteria are strings, so must be quoted.
When you pass a field via context API to your PostPage, you should avoid filter to all your gcms since your template has the information of that post, is not needed to redo the same previous query again (same than gatsby-node.js), it's not optimal. I will let it there since I don't know how is your data structured but should be refactored.
Applying it to your code should look like this.
gatsby-node.js:
exports.createPages = async ({ graphql, useStaticQuery, actions: { createPage } }) => {
const postQuery = graphql(`
{
gcms {
posts(filter: { stage: "PUBLISHED" }) {
id
slug
}
}
}
`);
let {posts}= postQuery.gcms;
posts.forEach(({ id, slug }) =>
createPage({
path: `/blog/${slug}`,
component: require.resolve(`./src/templates/PostPage.js`),
context: {
id: id,
slug: slug,
},
})
);
};
PostPage.js:
/* eslint-disable react/prop-types */
import React from 'react';
import { graphql } from 'gatsby';
import Layout from "../components/layout";
//import galaxy from "../images/galaxy.jpg";
import SEO from "../components/seo";
export const postPageQuery = graphql`
query PostPageQuery($id: ID!) {
gcms {
post(filter: { id: $id }) {
title
slug
excerpt
postContentMarkdown
tags
author {
name
biography
}
seo {
title
description
keywords
}
}
}
}
`;
const PostPage = ({data: {post}}) => {
return (
<Layout>
<SEO
keywords={[
`ui`,
`ux`,
]}
title="Blog" />
{post.slug}
</Layout>
);
};
export default PostPage;
I ended up fixing this by doing a complete recomposition of my project, with an update to the latest version of Gatsby, this with a bare bones gatsby starter, plugin by plugin. It ended up being plugin conflict issue. I'm not sure which plugin exactly it was, but most likely it was one of these:
gatsby-plugin-eslint, gatsby-plugin-offline, gatsby-plugin-root-import or possibly the prop-types NPM package.
I experienced this same issue after upgrading to the latest version of Gatsby.
Similarly to Tapha's answer, it was a plugin conflict for me. I had yet to upgrade the gatsby-source-strapi plugin. Upgrading that package to its latest available version solved the issue. Whatever your data source happens to be, I would check that it's still playing nice with Gatsby.
So, this warning/error message is very misleading, you can still use an exported query from templates, as detailed in the Gatsby documentation here.

Can you have complete coverage when using the pure component pattern to test Gatsby's static queries?

According to Gatsby docs, components that use static queries need to be split into variants: one being a pure component and the other that actually uses the static query, passing results to the pure component.
Given an example component:
import React from "react";
import { Helmet } from "react-helmet";
import { useStaticQuery, graphql } from "gatsby";
export const PureSEO = ({
description = ``,
lang = `en`,
meta = [],
title = ``,
image = ``,
...metaData
}): JSX.Element => {
const metaDescription = description;
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${title}`}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: title,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:creator`,
content: metaData.author,
},
{
name: `twitter:title`,
content: title,
},
{
name: `twitter:description`,
content: metaDescription,
},
{
name: `og:image`,
content: image,
},
{
name: `twitter:image`,
content: image,
},
{
name: `image`,
content: image,
},
].concat(meta)}
/>
);
};
const SEO: React.FC<Props> = ({
description = ``,
lang = `en`,
meta = [],
title = ``,
}): JSX.Element => {
const { site, image } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
title
description
author
rootDir
}
}
image: file(relativePath: { eq: "favicon.png" }) {
childImageSharp {
fixed(width: 400) {
...GatsbyImageSharpFixed
}
}
}
}
`
);
return (
<PureSEO
{...site.siteMetadata}
description={description}
image={image}
lang={lang}
meta={meta}
title={title}
/>
);
};
interface Props {
description?: string;
lang?: string;
meta?: HTMLMetaElement[];
title: string;
}
export default SEO;
I don't get full test coverage because SEO is not tested; but I don't think I should be testing that component, since the only logical difference is that it is using static queries.
How can I achieve better or 100% test coverage for my components, given this pattern?
Problem
I understand the intention of the Gatsby documentation avoid executing useStaticQuery, but the approach implicitly recommends leaving code uncovered.
Recommendation
To achieve code coverage, the most logical thing to do would be to either mock useStaticQuery or better yet use MockProvider.
GraphQL Testing: MockProvider - https://www.apollographql.com/docs/react/development-testing/testing/#mockedprovider
This problem is quite similar to how you would do unit testing of a backend component that communicates with some kind of external service, such as an SQL database.
You could of course mock useStaticQuery which will give you higher coverage, but it will not actually make you any surer that your code will work. What you want to test revolves around the correctness of your GraphQL query and whether the response format is what you expect, which is not something you can unit test.
So if you want to be certain that your code works you'd need to perform integration testing, e.g. by running Cypress tests against the built website.
Theoretically, you could also use contract testing to check that your query itself performs as expected, but it seems like an unnecessarily complicated solution for this usecase.

Gatsby: Index page "/" fails to load when using MDX, other path loads

I am using Gatsby to create a documentation site, using .mdx to manage contents. I have configured the gatsby-node.js and created the template. Everything path works fine, however, the index page fails to fetch results from the GraphQL query. I have cleared the cache and rebuilt the project severally. I tried a fix on Github which suggested renaming the index.js to main.js. This worked seamlessly on dev but not in prod.
Here's the gatsby-node.js file:
const {createFilePath} = require(`gatsby-source-filesystem`)
const path = require(`path`)
exports.createPages = ({actions, graphql}) => {
const {createPage} = actions
const blogPostTemplate = path.resolve('src/templates/postTemplate.js')
return(graphql(`
{
allMdx {
nodes {
frontmatter {
title
}
fields {
slug
}
}
}
}
`)).then(result => {
if(result.errors) {
throw result.errors
}
const posts = result.data.allMdx.nodes
posts.forEach(post => {
createPage({
path: post.fields.slug,
component: blogPostTemplate,
context: {
slug: post.fields.slug
}
})
})
})
}
exports.onCreateNode = ({node, actions, getNode}) => {
const { createNodeField } = actions
if(node.internal.type === `Mdx`) {
const value = createFilePath({node, getNode})
createNodeField({
name: `slug`,
node,
value
})
}
}
Here's the postTemplate.js file:
import React from 'react'
import {graphql} from 'gatsby'
import {MDXRenderer} from 'gatsby-plugin-mdx'
import Layout from '../components/Layout/Layout';
export default ({data}) => {
const {body, tableOfContents, fields} = data.mdx
return (
<Layout>
{tableOfContents}
<MDXRenderer>
{body}
</MDXRenderer>
{fields}
</Layout>
)
}
export const query = graphql`
query PostBySlug($slug: String!) {
mdx (fields: {slug: {eq: $slug}}) {
body
tableOfContents
fields {
slug
}
}
}
`
Any suggestions would be highly appreciated.
I found a solution to my issue. I discovered the index (/) page wasn't making use of the postTemplate.js. So my workaround was to copy the content of the postTemplate.js file into the index.js file. I then modified the graphql query to fetch just the / content. I don't know if this is the best solution, but it works fine now.
Use a ternary operator for your createPage path. I'm assuming your index's slug name is "index".
path: post.fields.slug === 'index' ? '/' : `/${post.fields.slug}`

Categories

Resources