GraphQL retrieve data for specific blocks - Gatsby + Wordpress - graphql

I have a React + Gatsby JS project that retrieves data from a Wordpress site through their headless API. I'm a total newbie to Gatsby.
Every page on my site is made up of blocks, which are in turn made up of fields. I'm using ACF to build these.
I am currently able to retrieve every page and a list of the blocks within that page by using the following GraphQL query:
query ($id: String!) {
currentPage: wordpressPage(id: {eq: $id}) {
title
acf {
page_blocks {
block_type {
acf_fc_layout
}
}
}
}
}
This returns the following data for page with id f4c4f4a7-ba0d-55b1-8877-16f543c22b80
{
"data": {
"wordpressPage": {
"id": "f4c4f4a7-ba0d-55b1-8877-16f543c22b80",
"acf": {
"page_blocks": [
{
"block_type": [
{
"acf_fc_layout": "page_title_and_text"
},
{
"acf_fc_layout": "two_column_media_and_text"
}
]
}
]
}
}
}
}
The blocks are next to afc_fc_layout. Both page_title_and_text and two_column_media_and_text are page blocks in that page.
Now, I would think that the next step would be to make a React component for each of those blocks, passing in the custom field data for each, to that component. If a page doesn't have a block, then there wouldn't be a need for me to retrieve the fields for that block, right?
Initially I thought I would run another query from my React component, requesting the fields for that particular block. But I realized I can't really add variables (page Id) to a static query within my components, per Gatsby docs, so I wouldn't be able to query that specific page for its fields. Please correct me if I'm wrong.
I believe I have to retrieve those fields I need from my main query that I've shown you here, but it seems absolutely bonkers to have to query for every possible custom field on the site, when not all pages are going to have the same blocks.
Ideally there would be some sort of syntax like
...
acf {
page_blocks {
block_type {
acf_fc_layout
if (acf_fc_layout eq page_title_and_text) {
title
text
}
if (acf_fc_layout eq two_column_media_and_text) {
media
text
}
}
}
}
...
And then I would pass those fields to their corresponding React component.
What is the proper way to go about this?
Note: I am currently at the point where I'm able to retrieve the fields from the API to render blocks. I am more wondering if there is any way my graphQL query can filter out the data for me, or if there is a way to customize the WP endpoint to show me field data filtered by the blocks that are actually on the page.
Ex: the site queries the data in blocks 4,3,2,10,12,15.... even though the page only has block 2.
I'm worried that devs that want to add blocks in the future will have to rewrite the query each time, hurting the site's scalability and potential performance.

You say you are a beginner with Gatsby but what you are trying to do touches many advanced topics inside Gatsby. My answer is most likely incomplete and you will need to figure many things out for yourself.
Prepare yourself for lots of documentation reading and lots of debugging to get things to work with Gatsby.
You want to programmatically create pages depending on the result of your GraphQL query. That means you need to create a page wide page template component.
In your templates folder of your Gatsby project, you create one template that programmatically picks the right components for each of your routes. To get your ACF data you use GraphQL page queries.
What is the proper way to go about this?
One alternative is this: You create React components that retrieve their data via props. You don't need to give each of those components their own GraphQL query since you already query in your page templates.
acf: acf_fc_layout eq page_title_and_text -> React component PageTitleAndText.jsx
const PageTitleAndText = ({ title, text}) => {
return (
<div>
<h1>{title}</h1>
<p>{text}</p>
</div>
);
};
// NO GraphQL query
export default PageTitleAndText;
Instead, you pass props inside your page template to your component:
acfPageTemplate.jsx
const acfPageTemplate = (props) => {
return (
<div>
{/* pass props as data from the GraphQL query result here */}
<PageTitleAndText title={props.data.currentPage.acf.page_blocks.block_type.acf_fc_layout.title }
text ={props.data.currentPage.acf.page_blocks.block_type.acf_fc_layout.text} />
</div>
);
};
export const query = graphql`
query ($id: String!) {
currentPage: wordpressPage(id: {eq: $id}) {
title
acf {
page_blocks {
block_type {
acf_fc_layout
}
}
}
}
}
`;
export default acfPageTemplate;
Define a page template for each of your acf layouts. Pick the right components for each layout and pass props as data from the GraphQL query result.
You need to pass variables to your page query. The only way to do this is to use page context as described in this question:
gatsby-node.js
createPage({
path: `/my-acf-page-title-and-text-page/`,
component: path.resolve(`./src/templates/PageTitleAndText.jsx`),
// The context is passed as props to the component as well
// as into the component's GraphQL query.
context: {
id: acfFieldId, // pass the acf field id
},
})
// define a createPage action for each of your acf layouts
But I realized I can't really add variables (page Id) to a static query within my components, per Gatsby docs, so I wouldn't be able to query that specific page for its fields. Please correct me if I'm wrong.
Correct. That's why you need to go the way with a page query, page template, and page context variable in gatsby-node.js
If a page doesn't have a block, then there wouldn't be a need for me to retrieve the fields for that block, right?
Yes. That's why you create a different page template for each of your acf layouts. You can create one big tempalte for all layouts but then you need to programmatically decide what components to add. This is out of scope of this question. You should ask a new question if you want to do this.
My advise is to get this to work with one specific layout before you go down this next rabbit hole, if you decide to do this at all.

Related

How to workaround non existent types in Graphql query in Gatsby

I'm building a website with a blog section, and on deployment to production the blog will be empty. I'm having problems allowing an empty blog on my Gatsby site.
When I run npm run develop it will only work if I have some blogs - I want it to work before the blogs have been added.
The main issues I'm encountering is trying to accomidate fields not existing like allStrapiBlog and strapiBlog because there are no blogs.
I get errors like this on blog components, and on my nav component (where i have a query that a conditional uses).
15:17 error Cannot query field "allStrapiBlog" on type "Query" graphql/template-strings
Cannot query field "strapiBlog" on type "Query"
This is what the query looks like for my navigation component. But it throws an error - is there a way to make it just return null?
query NavigationQuery {
allStrapiBlog {
nodes {
title
strapi_id
}
totalCount
}
}
How do I make unsuccessful GraphQL queries not break the build, so I can build a gatsby site with a empty blog?
But it throws an error - is there a way to make it just return null?
Indeed, you need to configure your GraphQL schema to allow nullable fields.
You have a boilerplate that you can tweak to match your data types at https://www.virtualbadge.io/blog-articles/nullable-relational-fields-strapi-gatsbyjs-graphql.
The idea relies on using the createSchemaCustomization API in your gatsbt-node.js to add your own type definitions.
Something like:
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions;
const typeDefs = `
type StrapiBlogPost implements Node {
title: String!
content: String
thumbnail: File
}
`;
createTypes(typeDefs);
};
In this case, the title is required (because of the !, which means that the type is non-nullable) while content and thumbnail can be null.
Afterward, you will only need to adapt your component to avoid code-breaking logics when null data is fetched.

How are arguments added to GraphQL, do they need to be defined before?

Hi Everyone I am just trying to learn graphql as I am using Gatsby. I want to know does each field in graphql take an argument or does it need to be defined somehow before. So for example if you visit this link graphql search results
https://graphql.org/swapi-graphql?query=%7B%0A%09allPeople%20%7B%0A%09%20%20people%20%7B%0A%09%20%20%20%20id%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20birthYear%0A%20%20%20%20%20%20eyeColor%0A%09%20%20%7D%0A%09%7D%0A%7D%0A
If i wanted to limit people by eye color how would I do that. In the docs it seems easy as you would just do something like people(eyecolor: 'brown') but that doesn't seem possible. Am I missing something? I basically want to do a SQL style search for all people where eye color is brown.
Thanks.
Arguments need to be defined in the schema and implemented in the resolver. If you're consuming a 3rd party API (like the link you provided), you're limited to their schema. You can tell by looking at their schema (by clicking Docs on the right side of the page) which fields take arguments. For example, person takes id and personID arguments:
people doesn't take any arguments, as seen in the schema:
If you're building your own schema, you can add arguments to any field, and when you implement the resolver for that field you can use the arguments for logic in that resolver.
If you're working with a schema that you don't control, you'll have to add filtering on the frontend:
const {people} = data.allPeople;
const brownEyedPeople = people.filter(({eyeColor}) => eyeColor === 'brown');
When you start developing in Gatsby and actually pull your data into Gatsby, there will be a filter query option that automatically becomes available in the query arguments.
https://www.gatsbyjs.org/docs/graphql-reference/#filter
You can expect to be able to filter your people by eyeColor by using the below query:
{
allPeople(filter: { eyeColor: { eq: "brown" } }) {
edges {
node {
id
name
birthYear
eyeColor
}
}
}
}

GatsbyJS passing user input to GraphQL

I’m looking for examples / tutorials on accepting user input from a form in GatsbyJS and passing that to my GraphQL query.
I can get the user input on submit and also pass variables in when testing graphiql, I just can’t figure out how to combine the two.
My data is stored in Drupal and is a list of recipes.
I’d like the user to be able to type in an ingredient e.g. chicken and then retrieve all of the recipes where chicken is an ingredient.
My query is
query SearchPageQuery($ingredient: String) {
allNodeRecipes(filter: {relationships: {field_ingredients: {elemMatch: {title: {eq: $ingredient}}}}}) {
edges {
node {
id
title
path {
alias
}
relationships {
field_ingredients {
title
}
}
}
}
}
}
If I’m understanding your question correctly, the short answer is you can’t, but another approach might work for you.
Gatsby’s GraphQL queries are run in advance as part of the static build of the site, so the data is part of the client-side JavaScript, but the queries have already been run by that point.
This is the same reason you can’t use JavaScript template literals in a StaticQuery:
// This doesn’t work
let myDynamicSlug = 'home'
return (
<StaticQuery
query={graphql`
query ExampleQuery {
examplePage(slug: { eq: ${myDynamicSlug} }) {
title
}
}
`}
render={data => {
console.log(data)
}}
/>
)
You’ll get an error message explaining “String interpolations are not allowed in graphql fragments.” Further reading: https://github.com/gatsbyjs/gatsby/issues/2293
I had a similar problem recently, and I realised it made a lot of sense why you can’t do this. If you are, ex. generating images using the queries in your GraphQL and things akin to that, you can’t pass in client side variables, because all the “static site” Gatsby operations like handling the images have are already done by that time.
What worked for me was to get the larger portion of data I needed in my query, and find what I needed within. In my previous example, that might mean getting allExamplePages instead of one examplePage, and then finding the myDynamicSlug I needed within it:
// This isn’t exactly how you’d hope to be able to do it,
// but it does work for certain problems
let myDynamicSlug = 'home'
return (
<StaticQuery
query={graphql`
query ExampleQuery {
# You might still be able to limit this query, ex. if you know your item
# is within the last 10 items or you don’t need any items before a certain date,
# but if not you might need to query everything
allExamplePages() {
edges {
node {
title
slug
}
}
}
}
`}
render={data => {
// Find and use the item you want, however is appropriate here
data.edges.forEach(item => {
if (item.node.slug === myDynamicSlug) {
console.log(item)
}
})
}}
/>
)
In your case, that hopefully there is an equivalent, ex. looking something up based on the user input. If you can be more specific about the structure of your data, I’d be happy to try and make my suggestion more specific. Hope that helps!

Unable to combine local and remote data in a single GraphQL query (Next.js + Apollo)

The setup:
My basic setup is a Next.js app querying data from a GraphQL API.
I am fetching an array of objects from the API and am able to display that array on the client.
I want to be able to filter the data based on Enum values that are defined in the API schema. I am able to pass these values programmatically and the data is correctly updated.
I want those filters to be persistent when a user leaves the page & come back. I was originally planning to use Redux, but then I read about apollo-link-state and the ability to store local (client) state into the Apollo store, so I set out to use that instead. So far, so good.
The problem:
When I try to combine the local query and the remote query into a single one, I get the following error: networkError: TypeError: Cannot read property 'some' of undefined
My query looks like this:
const GET_COMBINED = gql`
{
items {
id
details
}
filters #client
}
`
And I use it inside a component like this:
export default const Items = () => (
<Query query={GET_COMBINED}>
{({ loading, error, data: { items, filters } }) => {
...do stuff...
}}
</Query>
)
IF however, I run the queries separately, like the following:
const GET_ITEMS = gql`
{
items {
id
details
}
}
`
const GET_FILTERS = gql`
{
filters #client
}
`
And nest the queries inside the component:
export default const Items = () => (
<Query query={GET_ITEMS}>
{({ loading, error, data: { items } }) => {
return (
<Query query={GET_FILTERS}>
{({ data: { filters } }) => {
...do stuff...
}}
</Query>
)
}}
</Query>
)
Then it works as intended!
But it seems far from optimal to nest queries like this when a single query would - in theory, at least - do the job. And I truly don't understand why the combined query won't work.
I've stripped my app to its bare bones trying to understand, but the gist of it is, whenever I try to combine fetching local & remote data into a single query, it fails miserably, while in isolation both work just fine.
Is the problem coming from SSR/Next? Am I doing it wrong? Thanks in advance for your help!
Edit 2 - additional details
The error is triggered by react-apollo's getDataFromTree, however even when I choose to skip the query during SSR (by passing the ssr: false prop to the Query component), the combined query still fails. Besides, both the remote AND local queries work server-side when run separately. I am puzzled.
I've put together a small repo based on NextJS's with-apollo example that reproduces the problem here: https://github.com/jaxxeh/next-with-apollo-local
Once the app is running, clicking on the Posts (combined) link straight away will trigger an error, while Posts (split) link will display the data as intended.
Once the data has been loaded, the Posts (combined) will show data, but the attempt to load extra data will trigger an error. Reloading (i.e. server-rendering) the page will also trigger an error. Checkboxes will be functional and their state preserved across the app.
The Posts (split) page will fully function as intended. You can load extra post data, reload the page and set checkboxes.
So there is clearly an issue with the combined query, be it on the server-side (error on reload) or the client-side (unable to display additional posts). Direct writes to the local state (which bypass the query altogether) do work, however.
I've removed the Apollo init code for brevity & clarity, it is available on the repo linked above. Thank you.
Add an empty object as your resolver map to the config you pass to withClientState:
const stateLink = withClientState({
cache,
defaults: {
filters: ['A', 'B', 'C', 'D']
},
resolvers: {},
typedefs: `
type Query {
filters: [String!]!
}
`,
})
There's a related issue here. Would be great if the constructor threw some kind of error if the option was missing or if the docs were clearer about it.

Show data from Prismic.io in GatsbyJS frontend

I was playing around with prismic.io as a content source for a gatsby site and just can't figure out why I can't output certain data.
The graphql tool from gatsby returns all the right data so the query itself seems fine:
export const pageQuery = graphql`
query PageQuery {
allPrismicDocument {
edges {
node {
data{
product_image {
url
}
product_name {
text
}
product_price
product_description {
text
}
}
}
}
}
}
`
Now inside the page, when adding values such as:
{node.data.product_price}
{node.data.product_image.url}
it works just fine and outputs the correct data. However when I try:
{node.data.product_name.text}
or
{node.data.product_name}
I get nothing at all. Searched everywhere but there aren't many resources for using these two tools together yet:/
Any pointers would be much appreciated :)
I'm guessing that your product_name field is a Prismic Rich Text or Title field. If that's the case, then the field is an array of text blocks. You have two options for this:
Use the prismic-react development kit to help you display the field. Here is the Prismic documentation for this. This shows you how to use the RichText.render() and RichText.asText() helper functions to display this field type on a React front-end (which should work for Gatsby as well). It would look something like this:
{RichText.asText(node.data.product_name)}
If the field only has one block of text, you could just grab the first element of the array. Like this:
{node.data.product_name[0].text}

Resources