Gatsby 4 dynamic routes with Directus data seems imposible - graphql

I'm trying to create dynamic routing. Lets say we are building a blog.
The graphql data i get from Directus looks like this:
{
Directus {
Posts {
id
slug
Title
Body
}
}
}
and this seems to be a problem. No matter how i try to create the routes, Gatsby insists that the data structure should be with nodes instead of just an array. I have tried with the "File System Route API" which throws an error because of the missing nodes, the same thing happens if i try to define with "createPages" in gatsby-node.js.
Any help or suggestion is much appreciated...

I got it working... The solution is more simple then any guides i found on google, so here it comes, if anyone else ends up in the same situation.
Use the old cratePage method in a gatsby-node.js file, in root of project. I used this very simple code:
const path = require("path")
exports.createPages = async ({ graphql, actions }) => {
const { data } = await graphql(`
query Projects {
Directus {
Posts {
slug
}
}
}
`)
data.Directus.Posts.forEach(post => {
actions.createPage({
path: "/blog/" + post.slug,
component: path.resolve("./src/components/blog.js"),
context: { slug: post.slug },
})
})
}
And here's a link to useful information: https://www.youtube.com/watch?v=L32Vx_bEZhA

Related

Strapi GraphQL search by multiple attributes

I've got a very simple Nuxt app with Strapi GraphQL backend that I'm trying to use and learn more about GraphQL in the process.
One of my last features is to implement a search feature where a user enters a search query, and Strapi/GraphQL performs that search based on attributes such as image name and tag names that are associated with that image. I've been reading the Strapi documentation and there's a segment about performing a search.
So in my schema.graphql, I've added this line:
type Query {
...other generated queries
searchImages(searchQuery: String): [Image
}
Then in the /api/image/config/schema.graphql.js file, I've added this:
module.exports = {
query: `
searchImages(searchQuery: String): [Image]
`,
resolver: {
Query: {
searchImages: {
resolverOf: 'Image.find',
async resolver(_, { searchQuery }) {
if (searchQuery) {
const params = {
name_contains: searchQuery,
// tags_contains: searchQuery,
// location_contains: searchQuery,
}
const searchResults = await strapi.services.image.search(params);
console.log('searchResults: ', searchResults);
return searchResults;
}
}
}
},
},
};
At this point I'm just trying to return results in the GraphQL playground, however when I run something simple in the Playground like:
query($searchQuery: String!) {
searchImages(searchQuery:$searchQuery) {
id
name
}
}
I get the error: "TypeError: Cannot read property 'split' of undefined".
Any ideas what might be going on here?
UPDATE:
For now, I'm using deep filtering instead of the search like so:
query($searchQuery: String) {
images(
where: {
tags: { title_contains: $searchQuery }
name_contains: $searchQuery
}
) {
id
name
slug
src {
url
formats
}
}
}
This is not ideal because it's not an OR/WHERE operator, meaning it's not searching by tag title or image name. It seems to only hit the first where. Ideally I would like to use Strapi's search service.
I actually ran into this problem not to recently and took a different solution.
the where condition can be combined with using either _and or _or. as seen below.
_or
articles(where: {
_or: [
{ content_contains: $dataContains },
{ description_contains: $dataContains }
]})
_and
(where: {
_and: [
{slug_contains: $categoriesContains}
]})
Additionally, these operators can be combined given that where in this instance is an object.
For your solution I would presume you want an or condition in your where filter predicate like below
images(where: {
_or: [
{ title_contains: $searchQuery },
{ name_contains: $searchQuery }
]})
Lastly, you can perform a query that filters by a predicate by creating an event schema and adding the #search directive as seen here

GraphQL mutation in KeystoneJS: "Cannot use 'in' operator to search for 'id' in undefined"

In a KeystoneJS GraphQL project I'm trying to create a new data object (an "Article") in the 'resolveInput' hook of another, existing, data object (a "Proposal" -- when a Proposal is approved, I create an Article based on that Proposals'data).
This worked fine using the Mongoose adapter, but I've tried to do it using the built in GraphQL API, using keystone.executeQuery and I get the following error:
My Article list has a relationship with one Proposal (I'm leaving out the other fields)
fields: {
proposal: {
isUnique: false,
type: fields_1.Relationship,
ref: 'Proposal',
access: {
create: true,
read: true,
update: false
}
}
}
and I create the new Article thus (I'm omitting some code)
hooks: {
resolvedInput: async params => {
const articleCreateInput = {
title: cleanTitle,
text: cleanText,
visible: HIDDEN,
proposal: {
connect: {
id: proposalId
}
}
};
const articleCreationResult = await keystone.executeQuery(createArticle, { variables: articleCreateInput });
}
}
As far as I can see this is the correct way to do it, using connect, connecting an existing item to one you are creating.
My query is
const createArticle = `mutation createArticle($data:ArticleCreateInput) {
createArticle(data:$data) {
id
title
visible
publishDate
}
} `;
and as far as I can see I'm following the schema correctly
I'm sure I'm making an obvious mistake but at the moment I don't see it -- and I'm not sure whether that mistake is a GraphQL mistake or a KeystoneJS mistake (or both).

Optional queries in Gatsby / GraphQL

I have a Gatsby site with a contentful source, the initial page build GraphQL looks like this:
{
allContentfulProduct {
edges {
node {
slug
contentfulid
}
}
}
}
this works fine when using the preview API, but when using the production one it fails unless I have at least 1 Product entry published:
There was an error in your GraphQL query:
Cannot query field "allContentfulProduct" on type "Query". Did you mean ... [suggested entry names] ?
I'm pretty sure that when I publish a Product things will work as expected, but is there any way to make this query optional. The query should return zero results, and thus no Product pages will be created (expected outcome if no Product entries are published)
Try to add into your gatsby-node.js file and play around that.
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type allContentfulProduct implements Node{
slug: String,
contentfulid: String
}`

Use absolute path for featured image in markdown post with Gatsby

I've followed Gatsby tutorial for Working With Images in Markdown Posts and Pages which is working well but what I want to achieve is to fetch image from a static location instead of using a relative path for the image.
Would like to reference image like this (in frontmatter)
featuredImage: img/IMG_20190621_112048_2.jpg
Where IMG_20190621_112048_2.jpg is in /src/data/img instead of same directory as markdown file under /src/posts
I've tried to setup gatsby-source-filesystem like this :
{
resolve: `gatsby-source-filesystem`,
options: {
name: `posts`,
path: `${__dirname}/src/posts`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `data`,
path: `${__dirname}/src/data/`,
},
},
but graphQL query in post template fails :
export const query = graphql`
query($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
frontmatter {
title
featuredImage {
childImageSharp {
fluid(maxWidth: 800) {
...GatsbyImageSharpFluid
}
}
}
}
}
}
GraphQL Error Field "featuredImage" must not have a selection since
type "String" has no subfields.
Any idea how I could fetch image from a location distinct to the post markdown directory ?
Achieving this in Gatsby used to be pretty troublesome, but thanks to the new createSchemaCustomization Node API docs (since Gatsby 2.5) it's relatively easy.
Here's a demo where I replicate your repo structure: github
Here's where the relevant code lives: github
Here's the code to make it work:
// gatsby-node.js
const path = require('path')
exports.createSchemaCustomization = ({ actions }) => {
const { createFieldExtension, createTypes } = actions
createFieldExtension({
name: 'fileByDataPath',
extend: () => ({
resolve: function (src, args, context, info) {
const partialPath = src.featureImage
if (!partialPath) {
return null
}
const filePath = path.join(__dirname, 'src/data', partialPath)
const fileNode = context.nodeModel.runQuery({
firstOnly: true,
type: 'File',
query: {
filter: {
absolutePath: {
eq: filePath
}
}
}
})
if (!fileNode) {
return null
}
return fileNode
}
})
})
const typeDefs = `
type Frontmatter #infer {
featureImage: File #fileByDataPath
}
type MarkdownRemark implements Node #infer {
frontmatter: Frontmatter
}
`
createTypes(typeDefs)
}
How it works:
There are 2 parts to this:
Extend markdownRemark.frontmatter.featureImage so graphql resolves to a File node instead of a string via createTypes
Create a new field extension #fileByDataPath via createFieldExtension
createTypes
Right now Gatsby's inferring frontmatter.featureImage as a string. We'll ask Gatsby to read featureImage as a string instead, by modifying its parent type:
type Frontmatter {
featureImage: File
}
This is not enough however, we'll also need to pass this Frontmatter type to its parent as well:
type Frontmatter {
featureImage: File
}
type MarkdownRemark implements Node {
frontmatter: Frontmatter
}
We'll also add the #infer tag, which lets Gatsby know that it can infer other fields of these types, i.e frontmatter.title, markdownRemark.html, etc.
Then pass these custom type to createTypes:
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type Frontmatter #infer {
featureImage: File
}
type MarkdownRemark implements Node #infer {
frontmatter: Frontmatter
}
`
createTypes(typeDefs)
}
Now, we can fire up localhost:8000/___graphql and try to query the image
query Post {
markdownRemark {
frontmatter {
featureImage {
id
}
}
}
}
and we get...
Error: Cannot return null for non-nullable field File.id.
That is because while Gatsby now understands featureImage should be a File node, it has no idea where to get that file.
At this point, we can either use createResolvers to manually resolve the field to a File node, or createFileExtension to do the same thing. I choose createFileExtension because it allows more code reuse (you can extend any fields), while createResolvers, in this case, is more useful for a specific field. Seeing that all you want is to resolve a file from the src/data directory, I'll call this extension fieldByDataPath.
createFileExtension
Let's just look at the resolve attribute. It is a function that takes in the following:
source: The data of the parent field (in this case, frontmatter)
args: The arguments passed to featureImage in a query. We won't need this
context: contains nodeModel, which we'll use to get nodes from Gatsby node store
info: metadata about this field + the whole schema
We will find the original path (img/photo.jpg) from src.featureImage, then glue it to src/data to get a complete absolute path. Next, we query the nodeModel to find a File node with the matching absolute path. Since you have already pointed gatsby-source-filesystem to src/data, the image (photo.jpg) will be in Gatsby node store.
In case we can't find a path or a matching node, return null.
resolve: async function (src, args, context) {
// look up original string, i.e img/photo.jpg
const partialPath = src.featureImage
if (!partialPath) {
return null
}
// get the absolute path of the image file in the filesystem
const filePath = path.join(__dirname, 'src/data', partialPath)
// look for a node with matching path
const fileNode = await context.nodeModel.runQuery({
firstOnly: true,
type: 'File',
query: {
filter: {
absolutePath: {
eq: filePath
}
}
}
})
// no node? return
if (!fileNode) {
return null
}
// else return the node
return fileNode
}
We've done 99% of the work. The last thing to do is to move this to pass this resolve function to createFieldExtension; as well as add the new extension to createTypes
createFieldExtension({
name: 'fileByDataPath' // we'll use it in createTypes as `#fileByDataPath`
extend: () => ({
resolve, // the resolve function above
})
})
const typeDef = `
type Frontmatter #infer {
featureImage: File #fileByDataPath // <---
}
...
`
With that, you can now use relative path from src/data/ in frontmatter.
Extra
The way fileByDataPath implemented, it'll only work with fields named featureImage. That's not too useful, so we should modify it so that it'll work on any field that, say, whose name ended in _data; or at the very least accept a list of field names to work on.
Edit had a bit of time on my hand, so I wrote a plugin that does this & also wrote a blog on it.
Edit 2 Gatsby has since made runQuery asynchronous (Jul 2020), updated the answer to reflect this.
In addition to Derek Answer which allow assets of any type to be use anywhere (sound, video, gpx, ...), if looking for a solution only for images, one can use :
https://www.gatsbyjs.org/packages/gatsby-remark-relative-images/
The reason in your server schema you may have declared the featuredImage variable as string and in your client graphql query you are trying to call subobjects of the featuredImage variable and that subobjects is not existing.
You may have to check the graphql schema definition and align the query with the schema definition
you current schema might be like this
featuredImage: String
and you need to change it by declaring the proper types based on the requirements in the server side.
For more information about graphql types. please refer this url - https://graphql.org/learn/schema/#object-types-and-fields
Thanks
Rigin Oommen

Using map on returned graphql query is making known members undefined

I'm using Gatsbyjs to build a blog and I can't use the onCreatePage API to pass data from my graphql query into page templates.
My query grabs data from Kentico Cloud and it looks like this.
{
allKenticoCloudTypeBlogPost{
edges{
node{
contentItems{
elements{
url_slug{
value
}
}
}
}
}
}
}
This is a valid query and it returns data that looks like this.
The problem comes in my gatsby-node.js file where I want to utilize this query to build out pages using my predefined template.
Specifically in the createPage method which looks like this.
result.data.allKenticoCloudTypeBlogPost.edges.map(({node}) => {
createPage({
path: `${node.contentItems.elements.url_slug.value}`,
component: path.resolve(`./src/templates/blog-post.js`),
context: {
slug: node.contentItems.elements.url_slug.value,
}
})
});
The error that displays is the following.
TypeError: Cannot read property 'url_slug' of undefined
gatsby-node.js:31 result.data.allKenticoCloudTypeBlogPost.edges.map
C:/Users/xxxx/Desktop/Marketing Repos/xxxx/gatsby-node.js:31:57
I decided to investigate doing a console.table on node.contentItems, as it appears as though the elements part is where it gets tripped up.
The result of console.table(node.contentItems) just before the createPage method is this.
It appears that node.contentItems has a member called url_slug rather than the elements member that I expected.
I thought I could then solve my problem by updating my createPage method call like so.
result.data.allKenticoCloudTypeBlogPost.edges.map(({node}) => {
console.table(node.contentItems);
createPage({
path: `${node.contentItems.url_slug.value}`,
component: path.resolve(`./src/templates/blog-post.js`),
context: {
slug: node.contentItems.url_slug.value,
}
})
});
But then I get an error saying
TypeError: Cannot read property 'value' of undefined.
I truly don't understand how I can do a table log and see the url_slug member, but then when I try to access it, it says that it's undefined. All while I know that my query is correct because I can run it in graphiQL and get back the exact data I expect.
Any help would be appreciated. Thank you.
In your query result, node.contentItems is an array, even though you're trying to access it as if it's an object:
path: `${node.contentItems.elements.url_slug.value}`,
^^^^^^^^
console.log(contentItems) // [ { elements: {...} }, { elements: {...} }, ... ]
I think your confusion probably stems from the way console.table display data. It's confusing if you don't already know the shape of your data. Your screenshot says, this object has 4 properties with index 0 -> 3 (so likely an array), each has one property called elements (listed on table header), which is an object with the only property url_slug.
I'm not familiar with KenticoCloud, but maybe your posts are nested in contentItems, in which case you should loop over it:
result.data.allKenticoCloudTypeBlogPost.edges.map(({node}) => {
node.contentItems.forEach(({ elements }) => {
createPage({
path: elements.url_slug.value,
context: { slug: elements.url_slug.value },
component: ...
})
})
});
Is there a reason you are wrapping node with curly brackets in your map argument?
You might have already tried this, but my first intuition would be to do this instead:
result.data.allKenticoCloudTypeBlogPost.edges.map(node => {
console.log(node.contentItems)
createPage({
path: `${node.contentItems.elements.url_slug.value}`,
component: path.resolve(`./src/templates/blog-post.js`),
context: {
slug: node.contentItems.elements.url_slug.value,
}
})
});

Resources