Im currently having an issue when using multiple createPage routes in gatsby-node.js. I am trying to use Gatsby js with Shopify commerce storefront & and another CMS for blog articles so I need a way of creating routes when viewing products and when viewing blog articles respectively.
Currently, I am experiencing an error that only appears when trying to view a product detail page that reads:
(EnsureResources, ) TypeError: Cannot read property 'page' of undefined
My gatsby-node.js currently looks like this
const path = require(`path`)
// Create product page urls
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions
return graphql(`
{
allShopifyProduct {
edges {
node {
handle
}
}
}
}
`).then(result => {
result.data.allShopifyProduct.edges.forEach(({ node }) => {
const id = node.handle
createPage({
path: `/product/${id}/`,
component: path.resolve(`./src/templates/product-page.js`),
context: {
id,
},
})
})
})
}
// Create blog post slug urls
exports.createPages = async ({graphql, actions}) => {
const {createPage} = actions
const blogTemplate = path.resolve('./src/templates/blog.js')
const res = await graphql (`
query {
allContentfulBlogPost {
edges {
node {
slug
}
}
}
}
`)
res.data.allContentfulBlogPost.edges.forEach((edge) => {
createPage ({
component: blogTemplate,
path: `/blog/${edge.node.slug}`,
context: {
slug: edge.node.slug
}
})
})
}
You can't define the same API (createPages) twice. Do it in one function, especially since you can all put it into one query.
This code is obviously untested, but should work:
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const result = await graphql(`
{
shopify: allShopifyProduct {
nodes {
handle
}
}
contentful: allContentfulBlogPost {
nodes {
slug
}
}
}
`)
const shopifyTemplate = require.resolve(`./src/templates/product-page.js`)
const contentfulTemplate = require.resolve('./src/templates/blog.js')
if (result.errors) {
return
}
result.data.shopify.nodes.forEach(product => {
const id = product.handle
createPage({
path: `/product/${id}/`,
component: shopifyTemplate,
context: {
id,
},
})
})
result.data.contentful.nodes.forEach(post => {
createPage ({
component: contentfulTemplate,
path: `/blog/${post.slug}`,
context: {
slug: post.slug
}
})
})
}
The nodes is a shortcut for edges.node and valid syntax. The shopify: is an alias before the name of the query. You don't need to use path, you can also use require.resolve. The async/await syntax is better to read IMO.
Related
I'm able to run my code in dev just fine and works as expected. Not sure what I'm doing wrong. Am I unable to create nodes and then build pages from them in gatsby-node.js? Is there a better way of doing this in gatsby that I'm not aware of?
The problem is when I try to build I receive error.
"gatsby-node.js" threw an error while running the sourceNodes lifecycle:
Request failed with status code 400
Here is my code:
exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
}) => {
const { createNode } = actions
const auth_token = Buffer.from(
`${client_id}:${client_secret}`,
'utf-8'
).toString('base64')
const token_url = 'https://accounts.spotify.com/api/token'
const data = qs.stringify({ grant_type: 'client_credentials' })
const response = await axios.post(token_url, data, {
headers: {
Authorization: `Basic ${auth_token}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
})
const access_token = await response.data.access_token
const spotifyData = await axios.get(
`https://api.spotify.com/v1/artists/${artistId}/albums`,
{
headers: {
Authorization: `Bearer ${access_token}`,
'Content-type': 'application/json',
},
params: {
limit: 50,
market: 'US',
groups: 'single,album',
},
}
)
const ALBUM_NODE_TYPE = `Album`
await spotifyData.data.items.forEach(album =>
createNode({
...album,
id: createNodeId(`${ALBUM_NODE_TYPE}-${album.id}`),
parent: null,
children: [],
internal: {
type: ALBUM_NODE_TYPE,
contentDigest: createContentDigest(album),
},
})
)
}
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const results = await graphql(`
{
collections: allFile {
distinct(field: sourceInstanceName)
edges {
node {
id
sourceInstanceName
childMarkdownRemark {
id
fields {
slug
}
frontmatter {
title
image
}
}
}
}
}
news: allFile(filter: { sourceInstanceName: { eq: "news" } }) {
nodes {
sourceInstanceName
childMarkdownRemark {
frontmatter {
title
image
blurb
url
}
id
fields {
slug
}
}
}
}
music: allAlbum(sort: { fields: release_date, order: DESC }) {
nodes {
id
href
images {
url
}
name
artists {
name
}
release_date
album_type
external_urls {
spotify
}
}
}
}
`)
// Music
await results.data.music.nodes.forEach(node => {
console.log(node)
if (node.errors) {
node.errors.forEach(e => console.error(e.toString()))
return Promise.reject(node.errors)
}
const id = node.id
const name = node.name.replace(/\s+/g, '_').toLowerCase()
createPage({
path: `music/${name}`,
component: path.resolve(`src/templates/music/music.js`),
// additional data can be passed via context
context: {
id,
field: { ...node },
},
})
})
await results.data.news.nodes.forEach(node => {
if (node.errors) {
node.errors.forEach(e => console.error(e.toString()))
return Promise.reject(node.errors)
}
const id = node.childMarkdownRemark.id
createPage({
path: `${node.sourceInstanceName}${node.childMarkdownRemark.fields.slug}`,
component: path.resolve(
`src/templates/${String(node.sourceInstanceName)}/${String(
node.sourceInstanceName
)}.js`
),
// additional data can be passed via context
context: {
id,
field: { ...node.childMarkdownRemark.frontmatter },
},
})
})
//News Collection
await results.data.collections.distinct.forEach(collection => {
if (collection.errors) {
collection.errors.forEach(e => console.error(e.toString()))
return Promise.reject(collection.errors)
} else {
if (collection !== 'news') {
return
} else {
const edges = results.data.collections.edges
const nodes = edges.filter(
e => e.node.sourceInstanceName === 'news'
)
createPage({
path: `/collections/news`,
component: path.resolve(
`src/templates/news/collections/news.js`
),
context: {
nodes: { ...nodes },
},
})
}
}
})
}
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({ node, getNode })
createNodeField({
name: `slug`,
node,
value,
})
}
}
I also tried wrapping the createPage function in sourceNodes but that didn't work either.
I ended up creating a source-plugin and passing in my Spotify credentials as a string in gatbsy-config.js. It's not able to read env variable at build time but it is when running dev and I'm not sure I understand why that is.
I am fairly new to using graphql-ws and graphql-yoga server, so forgive me if this is a naive question or mistake from my side.
I went through graphql-ws documentation. It has written the schema as a parameter. Unfortunately, the schema definition used in the documentation is missing a reference.
After adding a new todo (using addTodo) it shows two todo items. So I believe it is unable to return the initial todo list whenever running subscribe on Yoga Graphiql explorer.
It should show the initial todo item as soon as it has been subscribed and published in the schema definition.
My understanding is there is something I am missing in the schema definition which is not showing the todo list when tried accessing Yoga Graphiql explorer.
Has anyone had a similar experience and been able to resolve it? What I am missing?
Libraries used
Backend
graphql-yoga
ws
graphql-ws
Frontend
solid-js
wonka
Todo item - declared in schema
{
id: "1",
title: "Learn GraphQL + Solidjs",
completed: false
}
Screenshot
Code Snippets
Schema definition
import { createPubSub } from 'graphql-yoga';
import { Todo } from "./types";
let todos = [
{
id: "1",
title: "Learn GraphQL + Solidjs",
completed: false
}
];
// channel
const TODOS_CHANNEL = "TODOS_CHANNEL";
// pubsub
const pubSub = createPubSub();
const publishToChannel = (data: any) => pubSub.publish(TODOS_CHANNEL, data);
// Type def
const typeDefs = [`
type Todo {
id: ID!
title: String!
completed: Boolean!
}
type Query {
getTodos: [Todo]!
}
type Mutation {
addTodo(title: String!): Todo!
}
type Subscription {
todos: [Todo!]
}
`];
// Resolvers
const resolvers = {
Query: {
getTodos: () => todos
},
Mutation: {
addTodo: (_: unknown, { title }: Todo) => {
const newTodo = {
id: "" + (todos.length + 1),
title,
completed: false
};
todos.push(newTodo);
publishToChannel({ todos });
return newTodo;
},
Subscription: {
todos: {
subscribe: () => {
const res = pubSub.subscribe(TODOS_CHANNEL);
publishToChannel({ todos });
return res;
}
},
},
};
export const schema = {
resolvers,
typeDefs
};
Server backend
import { createServer } from "graphql-yoga";
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/lib/use/ws";
import { schema } from "./src/schema";
import { execute, ExecutionArgs, subscribe } from "graphql";
async function main() {
const yogaApp = createServer({
schema,
graphiql: {
subscriptionsProtocol: 'WS', // use WebSockets instead of SSE
},
});
const server = await yogaApp.start();
const wsServer = new WebSocketServer({
server,
path: yogaApp.getAddressInfo().endpoint
});
type EnvelopedExecutionArgs = ExecutionArgs & {
rootValue: {
execute: typeof execute;
subscribe: typeof subscribe;
};
};
useServer(
{
execute: (args: any) => (args as EnvelopedExecutionArgs).rootValue.execute(args),
subscribe: (args: any) => (args as EnvelopedExecutionArgs).rootValue.subscribe(args),
onSubscribe: async (ctx, msg) => {
const { schema, execute, subscribe, contextFactory, parse, validate } =
yogaApp.getEnveloped(ctx);
const args: EnvelopedExecutionArgs = {
schema,
operationName: msg.payload.operationName,
document: parse(msg.payload.query),
variableValues: msg.payload.variables,
contextValue: await contextFactory(),
rootValue: {
execute,
subscribe,
},
};
const errors = validate(args.schema, args.document);
if (errors.length) return errors;
return args;
},
},
wsServer,
);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
apply these changes
Mutation: {
addTodo: (_: unknown, { title }: Todo) => {
const newTodo = {
id: "" + (todos.length + 1),
title,
completed: false
};
todos.push(newTodo);
publishToChannel({ todos });
return newTodo;
},
Subscription: {
todos: {
subscribe: () => {
return Repeater.merge(
[
new Repeater(async (push, stop) => {
push({ todos });
await stop;
}),
pubSub.subscribe(TODOS_CHANNEL),
]
)
}
},
},
first, npm i #repeaterjs/repeater then import Repeater
I want to generate pages from my GraphQl calls and I also want to enable the create page when the window path finds the app term, as you would when switching to a more app approach. I heard I could make it work by putting the if statement inside of the graphql promise I'm not really sure how. My build doesn't complete it gives me path is not defined. Any help is appreciated
const path = require(`path`)
// Log out information after a build is done
exports.onPostBuild = ({reporter}) => {
reporter.info(`Your Gatsby site has been built!`)
}
exports.createPages = ({page, actions, graphql}) => {
const {createPage} = actions
if (page.path.match(/^\/app/)) {
// page.matchPath is a special key that's used for matching pages
// with corresponding routes only on the client.
page.matchPath = '/app/*'
// Update the page.
createPage(page)
}
let query1 = new Promise((resolve, reject) => {
graphql(`
{
allDatoCmsBlog {
edges {
node {
slug
}
}
}
}
`).then(result => {
result.data.allDatoCmsBlog.edges.map(({node}) => {
createPage({
path: `blog/${node.slug}`,
component: path.resolve(`./src/templates/article.js`),
context: {
slug: node.slug,
},
})
})
resolve()
})
})
let query2 = new Promise((resolve, reject) => {
graphql(`
{
allDatoCmsBlog {
edges {
node {
slug
category
}
}
}
}
`).then(result => {
result.data.allDatoCmsBlog.edges.map(({node}) => {
createPage({
path: `blog/categoria/${node.category}`,
component: path.resolve(`./src/templates/gather-category.js`),
context: {
category: node.category,
},
})
})
})
resolve()
})
return query1, query2
}
Note that you are wrapping inside createPages instead of onCreatePage. You can keep both logics, but in two different export methods:
exports.createPages = ({page, actions, graphql}) => {
// your logic of page creation
}
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
if (page.path.match(/^\/app/)) {
page.matchPath = "/app/*"
createPage(page)
}
}
Alternatively, you can use the gatsby-plugin-create-client-paths plugin. In your gatsby-config.js:
{
resolve: `gatsby-plugin-create-client-paths`,
options: { prefixes: [`/app/*`] },
},
Simply as that.
Hi I'm using Gatsby starter blog.
I have two pages - blog and projects. Right now I have six posts, five blog posts and one project post. All the posts including the project post are created under /blog page. How do I ensure projects posts are created in /projects page instead?
I have added gatsby-source-filesystem with the path ${__dirname}/content/projects into my gatsby-config.js already.
Gatsby-node.js
const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === `MarkdownRemark`) {
const fileNode = getNode(node.parent);
const value = createFilePath({ node, getNode })
const parent = fileNode.sourceInstanceName;
let basePath = (parent === `blog`) ? `blog` : `projects`
createNodeField({
name: `slug`,
node,
value: `/${basePath}${value}`,
})
}
}
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions
const blogPost = path.resolve(`./src/templates/blog-post.js`)
const projectPost = path.resolve(`./src/templates/project-post.js`)
return graphql(
`
{
allMarkdownRemark(
sort: { fields: [frontmatter___date], order: DESC }
limit: 1000
) {
edges {
node {
fields {
slug
}
frontmatter {
title
}
}
}
}
}
`
).then(result => {
console.log(JSON.stringify(result, null, 4));
if (result.errors) {
throw result.errors
}
// Create blog and projects posts.
const posts = result.data.allMarkdownRemark.edges
posts.forEach((post, index) => {
const slug = post.node.fields.slug;
createPage({
path: `/${slug}`,
component: blogPost,
context: {
slug,
},
})
})
return null
})
}
I use Gatsby v1.0 to write my app.
Fot now I have I error in my console:
First of all it is strange, because 'children' has its' PropTypes, you can see component below:
import React from "react"
import Link from "gatsby-link"
import { Container } from "react-responsive-grid"
import { rhythm, scale } from "../utils/typography"
class Template extends React.Component {
render() {
const { location, children } = this.props
let header
if (location.pathname === "/") {
header = (
<h1 >
<Link to={"/"}>
Gatsby Starter Blog
</Link>
</h1>
)
} else {
header = (
<h3>
<Link to={"/"} >
Gatsby Starter Blog
</Link>
</h3>
)
}
return (
<Container>
{header}
{children()}
</Container>
)
}
}
Template.propTypes = {
children: React.PropTypes.function,
location: React.PropTypes.object,
route: React.PropTypes.object,
}
export default Template
Also, my project is not launch, bacause localhost shows error:
TypeError: Cannot read property 'markdownRemark' of undefined
I think something wrong in Node.js part, but all modules are installed and I am really sure in all connection with components, because it is starter kit and it is strange to see these mistakes here. Or maybe I should to fix previous component (they are in connect).
const _ = require("lodash")
const Promise = require("bluebird")
const path = require("path")
const select = require(`unist-util-select`)
const fs = require(`fs-extra`)
exports.createPages = ({ graphql, boundActionCreators }) => {
const { upsertPage } = boundActionCreators
return new Promise((resolve, reject) => {
const pages = []
const blogPost = path.resolve("./src/templates/blog-post.js")
resolve(
graphql(
`
{
allMarkdownRemark(limit: 1000) {
edges {
node {
fields {
slug
}
}
}
}
}
`
).then(result => {
if (result.errors) {
console.log(result.errors)
reject(result.errors)
}
// Create blog posts pages.
_.each(result.data.allMarkdownRemark.edges, edge => {
upsertPage({
path: edge.node.fields.slug, // required
component: blogPost,
context: {
slug: edge.node.fields.slug,
},
})
})
})
)
})
}
// Add custom slug for blog posts to both File and MarkdownRemark nodes.
exports.onNodeCreate = ({ node, boundActionCreators, getNode }) => {
const { addFieldToNode } = boundActionCreators
if (node.internal.type === `File`) {
const parsedFilePath = path.parse(node.relativePath)
const slug = `/${parsedFilePath.dir}/`
addFieldToNode({ node, fieldName: `slug`, fieldValue: slug })
} else if (node.internal.type === `MarkdownRemark`) {
const fileNode = getNode(node.parent)
addFieldToNode({
node,
fieldName: `slug`,
fieldValue: fileNode.fields.slug,
})
}
}