Add ImageSharp as a field to MarkdownRemark nodes (not frontmatter) - graphql

I have the following graphQL query I'm trying to get working:
{
allMarkdownRemark(
limit: 1000
) {
edges {
node {
id
parent {
id
}
fields{
slug
hero {
childImageSharp {
fixed {
src
}
}
}
}
frontmatter {
template
}
}
}
}
}
The hero field currently returns a path to an image using the following code:
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
// Add slug to MarkdownRemark node
if (node.internal.type === 'MarkdownRemark') {
const value = createFilePath({ node, getNode, basePath: 'library' })
const { dir } = getNode(node.parent)
const getHero = (d) => {
let hero = `${__dirname}/src/images/no-hero.gif`
if (fs.existsSync(`${d}/hero.jpg`)) hero = `${d}/hero.jpg`
if (fs.existsSync(`${d}/hero.png`)) hero = `${d}/hero.png`
if (fs.existsSync(`${d}/hero.gif`)) hero = `${d}/hero.gif`
return hero
}
createNodeField({
node,
name: 'slug',
value,
})
createNodeField({
node,
name: 'hero',
value: getHero(dir),
})
}
}
I've seen other people do something similar with an image path in the frontmatter but I don't want to have to use the frontmatter when it's easy enough to get graphql to see the file path without having to specify it.
However when I try the above I get the following error:
Field \"hero\" must not have a selection since type \"String\" has no
subfields.
Is there a way I can get childImageSharp to recognize this field?

I'm back again to (hopefully) settle this issue once and for all (see our history here).
This time, we'll attach the hero image's ImageSharp to the MarkdownRemark node. Your approach is correct, with 1 caveat: Gatsby seems to only recognize relative paths, i.e path starting with a dot.
You can fix this easily in your code:
const getHero = (d) => {
let hero = `${__dirname}/src/images/no-hero.gif`
- if (fs.existsSync(`${d}/hero.jpg`)) hero = `${d}/hero.jpg`
- if (fs.existsSync(`${d}/hero.png`)) hero = `${d}/hero.png`
- if (fs.existsSync(`${d}/hero.gif`)) hero = `${d}/hero.gif`
+ if (fs.existsSync(`${d}/hero.jpg`)) hero = `./hero.jpg`
+ if (fs.existsSync(`${d}/hero.png`)) hero = `./hero.png`
+ if (fs.existsSync(`${d}/hero.gif`)) hero = `./hero.gif`
return hero
}
createNodeField({
node,
name: 'hero',
value: getHero(dir),
})
This should work, though I want to provide an alternative hero search function. We can get a list of files in dir with fs.readdir, then find a file with the name 'hero':
exports.onCreateNode = async ({
node, actions,
}) => {
const { createNodeField } = actions
if (node.internal.type === 'MarkdownRemark') {
const { dir } = path.parse(node.fileAbsolutePath)
const heroImage = await new Promise((res, rej) => {
// get a list of files in `dir`
fs.readdir(dir, (err, files) => {
if (err) rej(err)
// if there's a file named `hero`, return it
res(files.find(file => file.includes('hero')))
})
})
// path.relative will return a (surprise!) a relative path from arg 1 to arg 2.
// you can use this to set up your default hero
const heroPath = heroImage
? `./${heroImage}`
: path.relative(dir, 'src/images/default-hero.jpg')
// create a node with relative path
createNodeField({
node,
name: 'hero',
value: `./${heroImage}`,
})
}
}
This way we don't care what the hero image's extension is, as long as it exists. I use String.prototype.includes, but you might want to use regex to pass in a list of allowed extensions, to be safe, like /hero.(png|jpg|gif|svg)/. (I think your solution is more readable, but I prefer to access the file system only once per node.)
You can also use path.relative to find the relative path to a default hero image.
Now, this graphql query works:
A (Minor) Problem
However, there's a minor problem with this approach: it breaks graphql filter type! When I try to query and filter based on hero, I get this error:
Perhaps Gatsby forgot to re-infer the type of hero, so instead of being a File, it is still a String. This is annoying if you need the filter to work.
Here's a workaround: Instead of asking Gatsby to link the file, we'll do it ourselves.
exports.onCreateNode = async ({
node, actions, getNode, getNodesByType,
}) => {
const { createNodeField } = actions
// Add slug to MarkdownRemark node
if (node.internal.type === 'MarkdownRemark') {
const { dir } = path.parse(node.fileAbsolutePath)
const heroImage = await new Promise((res, rej) => {
fs.readdir(dir, (err, files) => {
if (err) rej(err)
res(files.find(file => file.includes('hero')))
})
})
// substitute with a default image if there's no hero image
const heroPath = heroImage ? path.join(dir, heroImage) : path.resolve(__dirname, 'src/images/default-hero.jpg')
// get all file nodes
const fileNodes = getNodesByType('File')
// find the hero image's node
const heroNode = fileNodes.find(fileNode => fileNode.absolutePath === heroPath)
createNodeField({
node,
name: 'hero___NODE',
value: heroNode.id,
})
}
}
And now we can filter the hero field again:
If you don't need to filter content by hero image though, letting gatsby handle node type is much preferable.
Let me know if you run into issues trying this.

Related

How to add variables to graphQL query?

I am using Gatsby.
I need to increase the limit of 3 to each onCLick.
I've tried to follow this post, but with no success, so I removed the edits, and here is the original code...
This will help me to load more posts.
export const LatestNews = ({data}) => {
console.log(data);
return (
<section id="news_posts_section">
<p>some data</p>
</section>
);
};
export const latestNewsQuery = graphql`
query myquery{
allMarkdownRemark(
filter: { frontmatter: { layout: { eq: "news" } } }
sort: { fields: frontmatter___date, order: DESC }
limit: 2
) {
nodes {
frontmatter {
layout
title
path
date
featuredImage
thumbnail
}
excerpt(pruneLength: 325)
}
}
}
`;
Here is my gatsby-node:
exports.createPages = ({ actions, graphql }) => {
const { createPage } = actions;
const blogPostTemplate = path.resolve('src/templates/blog-post/BlogPost.js');
const newsTemplate = path.resolve('src/templates/news-single/NewsSingle.js');
const latestNewsPage = path.resolve(
'src/components/pages-implementation/news/sections/LatestNews.js',
);
const blog = graphql(`
{
blog: allMarkdownRemark(
filter: { frontmatter: { layout: { eq: "blog" } } }
) {
edges {
node {
frontmatter {
path
}
}
}
}
}
`).then((result) => {
if (result.errors) {
result.errors.forEach((e) => console.error(e.toString()));
return Promise.reject(result.errors);
}
const posts = result.data.blog.edges;
posts.forEach((edge) => {
const { path } = edge.node.frontmatter;
createPage({
path: path,
component: blogPostTemplate,
context: {},
});
});
});
const news = graphql(`
{
news: allMarkdownRemark(
filter: { frontmatter: { layout: { eq: "news" } } }
) {
edges {
node {
frontmatter {
path
}
}
}
}
}
`).then((result) => {
if (result.errors) {
result.errors.forEach((e) => console.error(e.toString()));
return Promise.reject(result.errors);
}
const news = result.data.news.edges;
news.forEach((edge) => {
const { path } = edge.node.frontmatter;
createPage({
path: path,
component: newsTemplate,
context: {},
});
});
news.forEach(edge => {
createPage({
path: `/news/`,
component: latestNewsPage,
context: {
// This time the entire product is passed down as context
product: edge
}
});
});
});
return Promise.all([blog, news]);
};
Edit 21 November:
I replaced the above code with my attempt to use a non-static query
I added the gatsby-node config
I give here a little explanation: the BlogPost.js and NewsSingle.js are templates that create new pages for each post or news post (from Netlify CMS)
The LatestNews.js is a component in a page. This is the main page of the news. Where are shown all the news? With a static query, it works fine, however, I need to make the "limit" a variable in order to apply a load more button, that onClick will increase the limit, thus showing more news posts on the loop.
With the above configuration it says:
warning The GraphQL query in the non-page component "/home/user/projectname/src/components/pages-implementation/news/sections/LatestNews.js" will not be run.
Exported queries are only executed for Page components. It's possible you're
trying to create pages in your gatsby-node.js and that's failing for some
reason.
useStaticQuery (hence the name) does not allow to receive variables. If you take a look at the docs:
useStaticQuery does not accept variables (hence the name “static”),
but can be used in any component, including pages
The only way to pass variables in a GraphQL query is by using the context API in the gatsby-node.js. For example:
queryResults.data.allProducts.nodes.forEach(node => {
createPage({
path: `/products/${node.id}`,
component: productTemplate,
context: {
// This time the entire product is passed down as context
product: node
}
});
});
};
In the snippet above, will be a product variable in the context with the whole node. It can be accessed through pageContext prop in the destination template or used as a query parameter.

How do I write a proper graphQL query to retrieve/filter 'tags' from ghost CMS in gatsby?

My goal is to filter posts in Gatsby from Ghost CMS using the GraphQL query with a "haka" tag.
The sample provided gatsbyghost is:
{
allGhostPost(filter: {tags: {elemMatch: {slug: {eq: $slug}}}}) {
edges {
node {
slug
...
}
}
}
}
I applied my query to file gatsby-node.js as follows:
const path = require(`path`)
const config = require(`./src/utils/siteConfig`)
const { paginate } = require(`gatsby-awesome-pagination`)
/**
* Here is the place where Gatsby creates the URLs for all the
* posts, tags, pages and authors that we fetched from the Ghost site.
*/
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions
/**
* Posts
*/
const createPosts = new Promise((resolve, reject) => {
const postTemplate = path.resolve(`./src/templates/post.js`)
const indexTemplate = path.resolve(`./src/templates/index.js`)
resolve(
graphql(`
{
allGhostPost(filter: {tags: {elemMatch: {slug: {eq: "haka"}}}}) (
sort: {order: ASC, fields: published_at}
) {
edges {
node {
slug
}
}
}
}`
).then((result) => {
if (result.errors) {
return reject(result.errors)
}
It comes out with error:
Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
Check the render method of PageRenderer.
Did I put the code in the wrong place? Or have I formatted incorrectly? Or do I need to update other files for the query to run?
Ahhh when I commented earlier I missed the syntax error in the graphql query. Would you try correcting it to see if that's the cause of the issue?
{
// remove this line
- allGhostPost(filter: {tags: {elemMatch: {slug: {eq: "haka"}}}}) (
// add this 2 lines instead
+ allGhostPost(
+ filter: {tags: {elemMatch: {slug: {eq: "haka"}}}},
sort: {order: ASC, fields: published_at}
) {
edges {
node {
slug
}
}
}
}
When passing in multiple arguments, you can separate them with the comma:
query {
allGhostPost(filter: ... , sort: ....) { ... }
}
Also, now that I see the full code, you should definitely remove the outer resolve:
const createPosts = new Promise((resolve, reject) => {
const postTemplate = path.resolve(`./src/templates/post.js`)
const indexTemplate = path.resolve(`./src/templates/index.js`)
- resolve(
...
- )
})
Not only it's unnecessary, it's also probably hiding the syntax error that graphql throw, which lead to your misleading error message.

How To Split Mobx State Tree Models Across Multiple Files?

I have a Mobx State Tree model that has grown too long and I would like to split it across multiple javascript files.
Here is a demo of some of the code:
///file1.js
import { types } from "mobx-state-tree";
export const ExampleModel = types
.model("Example", {
id: types.identifier,
name: types.optional(types.string, ""),
anotherName: types.optional(types.string, ""),
})
.views(self => ({
get test() {
return "test"
}
}))
.views(self => ({
get anotherTest() {
return "anotherTest"
}
}))
.actions(self => ({
setName(name) {
self.name = name
}
}))
.actions(self => ({
setAnotherName(name) {
self.anotherName = name
}
}))
What I want is to split this between two files, like:
///file1.js
import { types } from "mobx-state-tree";
export const ExampleModel = types
.model("Example", {
id: types.identifier,
name: types.optional(types.string, ""),
anotherName: types.optional(types.string, ""),
})
.views(self => ({
get test() {
return "test"
}
}))
.actions(self => ({
setName(name) {
self.name = name
}
}))
///file2.js
import { ExampleModel } from "./file1.js";
ExampleModel.views(self => ({
get anotherTest() {
return "anotherTest"
}
})).actions(self => ({
setAnotherName(name) {
self.anotherName = name
}
}))
You can see here that I am attempting to move a view and and action to a separate javascript file. I expect I need to do some kind of import and export between these two files, but I can't figure out how to do it.
I know that Mobx State Tree has compose functionality, as shown here:
https://nathanbirrell.me/notes/composition-mobx-state-tree/
But I am afer something more simple than this... I don't want to set up multiple models, I just need the ability to spread a model across multiple javascript files.
We do that all the time.
Just export your actions and views separately:
// file1.js
import { types } from "mobx-state-tree"
export const props = {
id: types.identifier,
name: types.optional(types.string, ""),
anotherName: types.optional(types.string, ""),
}
export const views = self => ({
get test() {
return "test"
}
})
export const actions = self => ({
setName(name) {
self.name = name
}
})
Then, create the final store from them:
// store.js
import { types } from "mobx-state-tree"
import * as file1 from "./file1"
import * as file2 from "./file2"
const Store = types
.model('Store')
.props(file1.props)
.views(file1.views)
.actions(file1.actions)
.props(file2.props)
.views(file2.views)
.actions(file2.actions)
export default Store
You can also create your own stores for testing, only from one file:
// __tests__/file1.js
import { types } from "mobx-state-tree"
import { actions, views, props } from "./file1"
const Store = types
.model('Store')
.props(props)
.views(views)
.actions(actions)
const store = Store.create(myTestSnapshot)
test('setName should set the name prop', () => {
store.setName('john')
expect(store.name).toBe('john')
})
Expressive, flexible and easy model composition is one of the best features in mobx-state-tree! :)
Here are two examples, taken straight from the relevant section in the docs:
const Square = types
.model("Square", {
width: types.number
})
.views(self => ({
surface() {
return self.width * self.width
}
}))
// create a new type, based on Square
const Box = Square
.named("Box")
.views(self => {
// save the base implementation of surface
const superSurface = self.surface
return {
// super contrived override example!
surface() {
return superSurface() * 1
},
volume() {
return self.surface * self.width
}
}
}))
// no inheritance, but, union types and code reuse
const Shape = types.union(Box, Square)
And another one:
const CreationLogger = types.model().actions(self => ({
afterCreate() {
console.log("Instantiated " + getType(self).name)
}
}))
const BaseSquare = types
.model({
width: types.number
})
.views(self => ({
surface() {
return self.width * self.width
}
}))
export const LoggingSquare = types
.compose(
// combine a simple square model...
BaseSquare,
// ... with the logger type
CreationLogger
)
// ..and give it a nice name
.named("LoggingSquare")
Applying that to your needs: Square and Box can be in different files, where Box.js imports Square from Square.js in the first example.
Same exact technique can be applied to the second example.

Gatsby - fetching remote images with createRemoteFileNode

I've been trying to fetch images from remote URL to Gatsby Source File system, to take advantage of lazy loading with gatsby-image plugin. I have a restful API which returns json with a string containing the image url. I followed this guide as I'm quite new to Gatsby Node Api and wasn't sure how to tackle this. Everything worked well until the point with adding additional properties to image with createNodeField. The properties seem to be added (I can see the object with fields property when I log the fileNode to the console. However, when trying to query the images, I get an error:
I'm wondering if there's something wrong in my code or is it due to the changes in gatsby? I'm using gatsby version 2.0.2. Is there a better option to somehow add additional properties to the image in order to be able to query just the needed ones?
Here's how my gatsby.node.js looks like:
const axios = require('axios');
const { createRemoteFileNode } = require(`gatsby-source-filesystem`);
exports.sourceNodes = ({ actions, createNodeId, node, store, cache } => {
const { createNode, createNodeField } = actions;
const processProject = project => {
project.photos.forEach(async photo => {
let fileNode;
try {
fileNode = await createRemoteFileNode({
url: photo.photo.url,
store,
cache,
createNode,
createNodeId: id => `projectPhoto-${photo.id}`,
});
await createNodeField({
node: fileNode,
name: 'ProjectPhoto',
value: 'true',
});
await createNodeField({
node: fileNode,
name: 'created_at',
value: photo.created_at,
});
} catch (error) {
console.warn('error creating node', error);
}
});
}
return axios.get(baseApiUrl).then(res => {
res.data.forEach(project => {
const nodeData = processProject(project);
createNode(nodeData);
});
});
}
In the end it seems that using .forEach with async/await was messing stuff up for some reason. Doing everything in for of loop, fixed the problem, although eslint was complaining about that a lot. Here's the code:
const axios = require('axios');
const { createRemoteFileNode } = require(`gatsby-source-filesystem`);
exports.sourceNodes = ({ actions, createNodeId, node, store, cache } => {
const { createNode, createNodeField } = actions;
const processProject = project => {
for (const photo of project.photos) {
let fileNode;
try {
fileNode = await createRemoteFileNode({
url: photo.photo.url,
store,
cache,
createNode,
createNodeId: id => `projectPhoto-${photo.id}`,
});
await createNodeField({
node: fileNode,
name: 'ProjectPhoto',
value: 'true',
});
await createNodeField({
node: fileNode,
name: 'created_at',
value: photo.created_at,
});
} catch (error) {
console.warn('error creating node', error);
}
}
}
return axios.get(baseApiUrl).then(res => {
res.data.forEach(project => {
const nodeData = processProject(project);
createNode(nodeData);
});
});
}

gatsbyjs query data from graphcms with status condition throw error object undefiend

Hello i have a gatsbyjs site that i tried to pull data of model 'job' from graphcms. if i pull alljob. the query works fine but if i try to put condition to pull only the job with the status field pubished. it didnt pull any data and throw an error:
TypeError: Cannot read property 'allJob' of undefined
Here's my gatsby-node.js:
const path = require(`path`);
const makeRequest = (graphql, request) => new Promise((resolve, reject) => {
resolve(
graphql(request).then(result => {
if (result.errors) {
reject(result.errors)
}
return result;
})
)
});
exports.createPages = ({ boundActionCreators, graphql }) => {
const { createPage } = boundActionCreators;
const getJobs = makeRequest(graphql, `
{
allJob(where: {status: PUBLISHED}) {
edges{
node{
id
}
}
}
}
`).then(result => { result.data.allJob.edges.forEach(({ node }) => {
createPage({
path: `/job/${node.id}`,
component: path.resolve(`src/templates/jobTemplate.js`),
context: {
id: node.id,
}
})
console.log(node.id)
})
}
)
return getJobs;
};
Gatsby doesn't understand allJob(where: {status: PUBLISHED}) as it's the wrong syntax.
You would want to use filter instead. I can't give you an example as I don't know how the structure is but can advise you to run gatsby develop and go to GraphiQL (http://localhost:8000/___graphql) and use it's autocomplete feature (Ctrl + Space) to get the right filter.
More information: https://www.gatsbyjs.org/docs/graphql-reference/#filter

Resources