Gatsby: Creating Pages from Contentful Fields - reactjs

Each of my posts on Contentful has some associated 'categories' with it. For example, one post might contain:
major: "HCD"
year: "1st Year"
tools: ["R", "Python", "Wordpress"]
These are just fields called major, year etc. with these values but they are treated as individual categories.
On the website, they are displayed as such:
I am trying to create a page for each of these categories. For example, if a user clicks on Photoshop, they should be taken to a page tags/photoshop and all posts containing that tag should be listed out.
Fortunately, I was able to find this guide to help me do this. However, the guide is not for Contentful data so I'm having a bit of trouble on how to do this. I have created the tagsTemplate.jsx and but I'm stuck at creating the actual pages.
For example, this is what I did to try and create pages for tools:
My gatsby-node.js file looks like this:
const path = require(`path`)
const _ = require('lodash');
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type contentfulPortfolioDescriptionTextNode implements Node {
description: String
major: String
author: String
tools: [String]
files: [ContentfulAsset]
contact: String
}
type ContentfulPortfolio implements Node {
description: contentfulPortfolioDescriptionTextNode
gallery: [ContentfulAsset]
id: ID!
name: String!
related: [ContentfulPortfolio]
slug: String!
major: String!
files: [ContentfulAsset]
author: String!
tools: [String]!
year: String!
thumbnail: ContentfulAsset
url: String
contact: String
}
`
createTypes(typeDefs)
}
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions
return new Promise((resolve, reject) => {
graphql(`
{
portfolio: allContentfulPortfolio {
nodes {
slug
tools
}
}
}
`).then(({ errors, data }) => {
if (errors) {
reject(errors)
}
if (data && data.portfolio) {
const component = path.resolve("./src/templates/portfolio-item.jsx")
data.portfolio.nodes.map(({ slug }) => {
createPage({
path: `/${slug}`,
component,
context: { slug },
})
})
}
const tools = data.portfolio.nodes.tools;
const tagTemplate = path.resolve(`src/templates/tagsTemplate.js`);
let tags = [];
// Iterate through each post, putting all found tags into `tags`
tags = tags.concat(tools);
// Eliminate duplicate tags
tags = _.uniq(tags);
// Make tag pages
tags.forEach(tag => {
createPage({
path: `/tags/${_.kebabCase(tag)}/`,
component: tagTemplate,
context: {
tag
},
});
});
console.log("Created Pages For" + tags)
resolve()
})
})
}
My tagsTemplate is minimal right now, since I don't know how to query the data:
import React from 'react';
import Layout from "../layouts/Layout"
const Tags = ({ data }) => {
return (
<Layout>
<div>Tags</div>
</Layout>
);
};
export default Tags;
The problem: When I visit the page for one of the tags I know exists (like photoshop), I get a 404. Why are these pages not being created?
What am I doing wrong and how can I fix it? How can this be generalized for three of my 'categories'?

According to what you said in the comments:
I tried console.log(tags) but is shows it as undefined
I did that and it is just a blank space. Does that mean there is nothing in tags at all?
Your contact function looks good, the approach is good since you are adding the tools (list of tags) into a new array to clean it up and leaving unique values (uniq). Once done, you loop through the unique tags and create pages based on that array.
That said, there are a few weak points where your house of cards can fall apart. Your issue start in this line:
const tools = data.portfolio.nodes.tools;
And propagates through the code.
nodes is an array so, to get any value you should do:
const tools = data.portfolio.nodes[0].tools;
To get the first position and so on...
Since tools is never populated, the rest of the code doesn't work.
You can easily fix it looping through the nodes and populating your tags array with something similar to:
const toolNodes = data.portfolio.nodes;
const tagTemplate = path.resolve(`src/templates/tagsTemplate.js`);
let tags = [];
// Iterate through each post, putting all found tags into `tags`
toolNodes.map(toolNode => tags.push(toolNode.tools);
// if the fetched data is still an array you can do toolNodes.map(toolNode => tags.push(...toolNode.tools);
// Eliminate duplicate tags
tags = _.uniq(tags);
// Make tag pages
tags.forEach(tag => {
createPage({
path: `/tags/${_.kebabCase(tag)}/`,
component: tagTemplate,
context: {
tag
},
});
});
console.log("Created Pages For" + tags)

Related

Crate Tag & Category taxonomy in Gatsby

I have a development on Gatsby with allMdx. I created a "Category" taxonomy and to create a category page I used a file gatsby-node.js . There's such a code inside.
const _ = require("lodash")
const { transliterate } = require('./src/functions/transletter');
function dedupeCategories(allMdx) {
const uniqueCategories = new Set()
// Iterate over all articles
allMdx.edges.forEach(({ node }) => {
// Iterate over each category in an article
node.frontmatter.categories.forEach(category => {
uniqueCategories.add(category)
})
})
// Create new array with duplicates removed
return Array.from(uniqueCategories)
}
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions
// Query markdown files including data from frontmatter
const { data: { allMdx } } = await graphql(`
query {
allMdx {
edges {
node {
id
frontmatter {
categories
tags
slug
}
}
}
}
}
`)
// Create array of every category without duplicates
const dedupedCategories = dedupeCategories(allMdx)
// Iterate over categories and create page for each
dedupedCategories.forEach(category => {
reporter.info(`Creating page: blog/category/${category}`)
createPage({
path: `blog/category/${_.kebabCase(transliterate(category))}`,
component: require.resolve("./src/templates/categories.js"),
// Create props for our CategoryList.js component
context: {
category,
// Create an array of ids of articles in this category
ids: allMdx.edges
.filter(({ node }) => {
return node.frontmatter.categories.includes(category)
})
.map(({node}) => node.id),
},
})
})
}
Now I want to create a "Tag" taconomy, but I can't figure out how to do it beautifully and briefly, what and where to add to the gatsby-node.js so that I have two taxonomies created that work the same way as one. It is clear that you can simply duplicate this code and write "tag" instead of "category", but this is not very nice.
Just in case, here is my template code category.js
import React from "react"
import { Link, graphql } from "gatsby"
import Layout from '../components/layout'
import Seo from '../components/seo'
const CategoryList = ({ pageContext: { category }, data: { allMdx }, }) =>
(
<Layout pageTitle={category}>
{
allMdx.edges.map(({ node }) => {
return (
<article key={node.id}>
<h2>
<Link to={`/blog/${node.frontmatter.slug}`}>
{node.frontmatter.title}
</Link>
</h2>
<p>Posted: {node.frontmatter.date}</p>
<p>{node.excerpt}</p>
</article>
)
})
}
</Layout>
)
export const query = graphql`
query CategoryListQuery($ids: [String]!) {
allMdx (filter: { id: { in: $ids } }) {
edges {
node {
frontmatter {
title
date(formatString: "MMMM DD, YYYY")
slug
}
id
excerpt
}
}
}
}
`
export const Head = ({ pageContext }) => (
<Seo
title={pageContext.category}
description={`Статьи из категории ${pageContext.category}`}
/>
)
export default CategoryList
I don't see anything wrong in your approach. In fact, is the way to go in terms of getting all tags and categories (hence a map) and loop through them to create the pages, at least given your approach.
However, I think you can save some steps if you change your markdown structure: if each MDX has a key attribute (or similar) containing the type of taxonomy it is (article, tag, category, page, etc) you can create more succinct GraphQL queries, hence you can save the filters.
For instance, in that way, you would be able to create a single query tags and categories:
const tagsQuery = await graphql(`
query getAllTags {
allTags: allMarkdownRemark (
filter: { frontmatter: { key: { in: ["tag"] }}}) {
edges {
node {
id
frontmatter {
name
slug
type
key
}
}
}
}
}
`);
Note: you can use eq operator instead of in. In this case the array will accept more types of tags like projectTags, articleTags, etc.
This allows you to create more specific approach because your data will contain allTags and allCategories, so you can create a general dedupeCategories (which will be named dedupeMdx) which no matter the input, will return a unique array (of tags or categories) because you don't care about the data, all MDX will have the same internal structure to the loop and the function can be agnostic to that.
Following that approach, you can omit the following filter:
context: {
category,
// Create an array of ids of articles in this category
ids: allMdx.edges
.filter(({ node }) => {
return node.frontmatter.categories.includes(category)
})
.map(({node}) => node.id),
},
The filter won't be necessary if you pass the dedupedCategories array (and so with the tags) and use a filter GraphQL in the template query, which in fact is what you would do either way, so you are saving one step.
In other words, you create pages for each category (or tag), pass the array of categories via context and get, from allMdx (filtered by the key + the unique array) the needed data.

TypeError: result.data.umdHub.articles.forEach is not a function

I am having issues linking pages with slugs. All I am aiming to do is create a page with a list of articles (which I have). But I cannot link those articles to display their content. I understand you might need to use createPages. Below is the code I am trying. Does anyone have experience with this that might be able to point me in the right direction for linking index and article pages?
exports.createPages = ({ graphql, actions }) => {
// **Note:** The graphql function call returns a Promise
// see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise for more info
const { createPage } = actions
return graphql(`
{
umdHub {
articles {
data {
slug
title
body
subtitle
hero_image {
url_1200_630
}
}
}
}
}
`
).then(result => {
result.data.umdHub.articles.forEach(({ data }) => {
createPage({
path: articles.data.slug,
component: path.resolve(`./src/article.js`),
context: {
// Data passed to context is available
// in page queries as GraphQL variables.
slug: articles.data.slug,
},
})
})
})
}
I am getting this error with the above code:
TypeError: result.data.umdHub.articles.forEach is not a function
Second Attempt:
const path = require(`path`)
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions
const articleTemplate = path.resolve(`./src/terp/article.js`)
// Query for markdown nodes to use in creating pages.
// You can query for whatever data you want to create pages for e.g.
// products, portfolio items, landing pages, etc.
return graphql(`
{
umdHub {
articles {
data {
id
title
subtitle
body
summary
hero_image {
url_1200_630
}
authorship_date {
formatted_short
unix
unix_int
formatted_long
formatted_short
time
}
slug
}
}
}
}
`).then(result => {
if (result.errors) {
throw result.errors
}
// Create blog post pages.
result.data.umdHub.articles.data.forEach(data => {
createPage({
// Path for this page — required
path: `${data.slug}`,
component: articleTemplate,
context: {
// Add optional context data to be inserted
// as props into the page component..
//
// The context data can also be used as
// arguments to the page GraphQL query.
//
// The page "path" is always available as a GraphQL
// argument.
},
})
})
})
}
Returns error:
⠐ createPages Your site's "gatsby-node.js" created a page with a
component that doesn't exist

How to query relational data with GraphQL, Firebase and Gatsby

I'm building a Gatsby.js site.
The site uses the gatsby-source-firestore plugin to connect to the Firestore data source.
My question is this. How can I query relational data? As in, fetch data from two models at once, where modelA[x] = modelB[y]
I don't really understand resolvers. I don't think I have any.
Note, I am not considering graph.cool currently. I'd like to stick with Firebase. I will do the relational data matching in pure JS if I have to (not GraphQL).
Here is what my gatsby-config.js looks like:
{
resolve: 'gatsby-source-firestore',
options: {
credential: require('./firebase-key.json'),
databaseURL: 'https://testblahblah.firebaseio.com',
types: [
{
type: 'Users',
collection: 'users',
map: user => ({
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
ownsHat: user.ownsHat,
hatId: user.hatId
})
},
{
type: 'Hats',
collection: 'hats',
map: hat => ({
hatType: hat.hatType,
hatUserId: hat.hatUserId,
hatId: hat.hatId
})
}
]
}
},
This pulls in two flat data models. I can query like this in-page:
any-page.js
export const query = graphql`
query {
allUsers {
edges {
node {
...UserFragment
}
}
}
}
`
What I'm looking for is a query that lets me write one query inside another i.e. a relational data query within a query.
export const query = graphql`
query {
allUsers {
edges {
node {
...UserFragment {
hats (user.userId == hat.userId){
type
hatId
}
}
}
}
}
}
`
As you can understand, this amounts to: How to run multiple GraphQL queries of relational data.
Given the nature of Firestore's flat JSON, this makes the relational aspect of GraphQL difficult.
I'm really keen to understand this better and would really appreciate being pointed down the right path.
I am really keen on sticking with GraphQL and Firebase.
Thanks!
I'm not sure this works in graphql but in Gatsby you can use gatsby-node to create and alter your nodes and inject hats to each user node. Here's an example code I'm using to add authors to a Post node:
const mapAuthorsToPostNode = (node, getNodes) => {
const author = getPostAuthorNode(node, getNodes);
if (author) node.authors___NODES = [author.id];
};
exports.sourceNodes = ({actions, getNodes, getNode}) => {
const {createNodeField} = actions;
getCollectionNodes('posts', getNodes).forEach(node => {
mapAuthorsToPostNode(node, getNodes);
});
};
This is one way to do it provided the records are not in huge numbers. If they are, you should create a hats page to display user hats where you query just the hats filtered by user id which is received via a paceContext param such as user id.
export const pageQuery = graphql`
query($userId: String) {
hats: allHats(
filter: {
userId: {eq: $userId}
}
) {
edges {
node {
...hatFragment
}
}
}
}
`;

GatsbyJS getting data from Restful API

I am new in both React and GatsbyJS. I am confused and could not make figuring out in a simple way to load data from third-party Restful API.
For example, I would like to fetch data from randomuser.me/API and then be able to use the data in pages.
Let’s say something like this :
import React from 'react'
import Link from 'gatsby-link'
class User extends React.Component {
constructor(){
super();
this.state = {
pictures:[],
};
}
componentDidMount(){
fetch('https://randomuser.me/api/?results=500')
.then(results=>{
return results.json();
})
.then(data=>{
let pictures = data.results.map((pic,i)=>{
return(
<div key={i} >
<img key={i} src={pic.picture.medium}/>
</div>
)
})
this.setState({pictures:pictures})
})
}
render() {
return (<div>{this.state.pictures}</div>)
}
}
export default User;
But I would like to get the help of GraphQL in order to filter & sort users and etc…..
Could you please help me to find the sample to how I can fetch data and insert them into GraphQL on gatsby-node.js?
If you want to use GraphQL to fetch your data, you have to create a sourceNode. The doc about creating a source plugin could help you.
Follow these steps to be able to query randomuser data with GraphQL in your Gatsby project.
1) Create nodes in gatsby-node.js
In your root project folder, add this code to gatsby-node.js:
const axios = require('axios');
const crypto = require('crypto');
exports.sourceNodes = async ({ actions }) => {
const { createNode } = actions;
// fetch raw data from the randomuser api
const fetchRandomUser = () => axios.get(`https://randomuser.me/api/?results=500`);
// await for results
const res = await fetchRandomUser();
// map into these results and create nodes
res.data.results.map((user, i) => {
// Create your node object
const userNode = {
// Required fields
id: `${i}`,
parent: `__SOURCE__`,
internal: {
type: `RandomUser`, // name of the graphQL query --> allRandomUser {}
// contentDigest will be added just after
// but it is required
},
children: [],
// Other fields that you want to query with graphQl
gender: user.gender,
name: {
title: user.name.title,
first: user.name.first,
last: user.name.last,
},
picture: {
large: user.picture.large,
medium: user.picture.medium,
thumbnail: user.picture.thumbnail,
}
// etc...
}
// Get content digest of node. (Required field)
const contentDigest = crypto
.createHash(`md5`)
.update(JSON.stringify(userNode))
.digest(`hex`);
// add it to userNode
userNode.internal.contentDigest = contentDigest;
// Create node with the gatsby createNode() API
createNode(userNode);
});
return;
}
I used axios to fetch data so you will need to install it: npm install --save axios
Explanation:
The goal is to create each node for each piece of data you want to use.
According to the createNode documentation, you have to provide an object with few required fields (id, parent, internal, children).
Once you get the results data from the randomuser API, you just need to create this node object and pass it to the createNode() function.
Here we map to the results as you wanted to get 500 random users https://randomuser.me/api/?results=500.
Create the userNode object with the required and wanted fields.
You can add more fields depending on what data you will want to use in your app.
Just create the node with the createNode() function of the Gatsby API.
2) Query your data with GraphQL
Once you did that, run gatsby develop and go to http://localhost:8000/___graphql.
You can play with GraphQL to create your perfect query. As we named the internal.type of our node object 'RandomUser', we can query allRandomUser to get our data.
{
allRandomUser {
edges {
node {
gender
name {
title
first
last
}
picture {
large
medium
thumbnail
}
}
}
}
}
3) Use this query in your Gatsby page
In your page, for instance src/pages/index.js, use the query and display your data:
import React from 'react'
import Link from 'gatsby-link'
const IndexPage = (props) => {
const users = props.data.allRandomUser.edges;
return (
<div>
{users.map((user, i) => {
const userData = user.node;
return (
<div key={i}>
<p>Name: {userData.name.first}</p>
<img src={userData.picture.medium} />
</div>
)
})}
</div>
);
};
export default IndexPage
export const query = graphql`
query RandomUserQuery {
allRandomUser {
edges {
node {
gender
name {
title
first
last
}
picture {
large
medium
thumbnail
}
}
}
}
}
`;
That is it!
Many thanks, this is working fine for me, I only change small parts of the gastbyjs-node.js because it makes an error when use sync & await, I think I need change some section of a build process to use babel to allow me to use sync or await.
Here is the code which works for me.
const axios = require('axios');
const crypto = require('crypto');
// exports.sourceNodes = async ({ boundActionCreators }) => {
exports.sourceNodes = ({boundActionCreators}) => {
const {createNode} = boundActionCreators;
return new Promise((resolve, reject) => {
// fetch raw data from the randomuser api
// const fetchRandomUser = () => axios.get(`https://randomuser.me/api/?results=500`);
// await for results
// const res = await fetchRandomUser();
axios.get(`https://randomuser.me/api/?results=500`).then(res => {
// map into these results and create nodes
res.data.results.map((user, i) => {
// Create your node object
const userNode = {
// Required fields
id: `${i}`,
parent: `__SOURCE__`,
internal: {
type: `RandomUser`, // name of the graphQL query --> allRandomUser {}
// contentDigest will be added just after
// but it is required
},
children: [],
// Other fields that you want to query with graphQl
gender: user.gender,
name: {
title: user.name.title,
first: user.name.first,
last: user.name.last
},
picture: {
large: user.picture.large,
medium: user.picture.medium,
thumbnail: user.picture.thumbnail
}
// etc...
}
// Get content digest of node. (Required field)
const contentDigest = crypto.createHash(`md5`).update(JSON.stringify(userNode)).digest(`hex`);
// add it to userNode
userNode.internal.contentDigest = contentDigest;
// Create node with the gatsby createNode() API
createNode(userNode);
});
resolve();
});
});
}
The accepted answer for this works great, just to note that there's a deprecation warning if you use boundActionCreators. This has to be renamed to actions to avoid this warning.
You can get data at the frontend from APIs using react useEffect. It works perfectly and you will no longer see any error at builtime
const [starsCount, setStarsCount] = useState(0)
useEffect(() => {
// get data from GitHub api
fetch(`https://api.github.com/repos/gatsbyjs/gatsby`)
.then(response => response.json()) // parse JSON from request
.then(resultData => {
setStarsCount(resultData.stargazers_count)
}) // set data for the number of stars
}, [])
The answers given above work, except the query in step 2 seems to only return one node for me. I can return all nodes by adding totalCount as a sibling of edges. I.e.
{
allRandomUser {
totalCount
edges {
node {
id
gender
name {
first
last
}
}
}
}
}

readQuery not working with pagination in Apollo & GraphQL app

I've got the following setup for my app. I have a LinkList component that renders a list of Link components. Then I also have a CreateLink component to create new links. Both are rendered under different routes with react-router:
<Switch>
<Route exact path='/create' component={CreateLink}/>
<Route exact path='/:page' component={LinkList}/>
</Switch>
The Link type in my GraphQL schema looks as follows:
type Link implements Node {
url: String!
postedBy: User! #relation(name: "UsersLinks")
votes: [Vote!]! #relation(name: "VotesOnLink")
comments: [Comment!]! #relation(name: "CommentsOnLink")
}
I'm using Apollo Client and want to use the imperative store API to update the list after new Link was created in the CreateLink component.
await this.props.createLinkMutation({
variables: {
description,
url,
postedById
},
update: (store, { data: { createLink } }) => {
const data = store.readQuery({ query: ALL_LINKS_QUERY }) // ERROR
console.log(`data: `, data)
}
})
The problem is that store.readQuery(...) throws an error:
proxyConsole.js:56 Error: Can't find field allLinks({}) on object (ROOT_QUERY) {
"allLinks({\"first\":2,\"skip\":10})": [
{
"type": "id",
"id": "Link:cj3ucdguyvzdq0131pzvn37as",
"generated": false
}
],
"_allLinksMeta": {
"type": "id",
"id": "$ROOT_QUERY._allLinksMeta",
"generated": true
}
}.
Here is how I am fetching the list of links in my LinkList component:
export const ALL_LINKS_QUERY = gql`
query AllLinksQuery($first: Int, $skip: Int) {
allLinks(first: $first, skip: $skip) {
id
url
description
createdAt
postedBy {
id
name
}
votes {
id
}
}
_allLinksMeta {
count
}
}
`
export default graphql(ALL_LINKS_QUERY, {
name: 'allLinksQuery',
options: (ownProps) => {
const { pathname } = ownProps.location
const page = parseInt(pathname.substring(1, pathname.length))
return {
variables: {
skip: (page - 1) * LINKS_PER_PAGE,
first: LINKS_PER_PAGE
},
fetchPolicy: 'network-only'
}
}
}) (LinkList)
I am guessing that the issue somehow has to do with my pagination approach, but I still don't know how to fix it. Can someone point me into the right direction here?
How to read a paginated list from the store depends on how you do the pagination. If you're using fetchMore, then all the data will be stored under the original keys of the query, which in this case I guess was fetched with { first: 2, skip: 0 }. That means in order to read the updated list from the store, you would have to use the same parameters, using { first: 2, skip: 0 } as variables.
PS: The reason Apollo does it this way is because it still allows you to relatively easily update a list via a mutation or update store. If each page was stored separately, it would be very complicated to insert an item in the middle or the beginning of the list, because all of the pages would potentially have to be shifted.
That said, we might introduce a new client-side directive called #connection(name: "ABC") which would let you explicitly specify under which key the connection is to be stored, instead of automatically storing it under the original variables. Happy to talk more about it if you want to open an issue on Apollo Client.

Resources