(Gatsby + Prismic): Content Relationship URLs - reactjs

I have a Gatsby Prismic blog with two custom content types - guide, and review.
I want the URL structure to be www.mysite.com/guide_url/review_url.
What I've done so far:
I've added a content relationship field in Prismic to the parent custom type guide and linked the children (review) to it.
In my gatsby-node.js file the code looks like this:
exports.createPages = ({ graphql, boundActionCreators }) => {
const { createPage } = boundActionCreators;
return new Promise((resolve, reject) => {
graphql(`
{
allPrismicGuide {
edges {
node {
id
uid
}
}
}
allPrismicReview {
edges {
node {
id
uid
}
}
}
`).then(result => {
const categoriesSet = new Set();
result.data.allPrismicGuide.edges.forEach(({ node }) => {
createPage({
path: node.uid,
component: path.resolve(`./src/templates/GuideTemplate.js`),
context: {
// Data passed to context is available in page queries as GraphQL variables.
slug: node.uid
}
});
});
result.data.allPrismicReview.edges.forEach(({ node }) => {
createPage({
path: node.uid,
component: path.resolve(`./src/templates/ReviewTemplate.js`),
context: {
// Data passed to context is available in page queries as GraphQL variables.
slug: node.uid,
tags: node.tags
}
});
});
I'm pretty new to Gatsby and Prismic.
I would love to get some feedback (helpful links) and suggestions!

[Update] Prismic is not longer recommending the gatsby-source-prismic-graphql plugin.
Here's an article that'll help you migrating to the other one:
How to migrate a project from 'gatsby-source-prismic' to 'gatsby-source-prismic-graphql'

The Prismic docs are now recommending the gatsby-source-prismic plugin.
If your relationship field is named review your query should look something like this:
{
allPrismicGuide {
edges {
node {
id
uid
data {
review {
document {
... on PrismicReview {
uid
}
}
}
}
}
}
}
}
Then you should be able to access the child uid in node.data.review.document.uid. So:
createPage({
path: `${node.uid}/${node.data.review.document.uid}`,
// ... other stuff ...
});
In your linkResolver:
if (doc.type === 'guide') {
return `${doc.uid}/${doc.data.review.uid}`;
}
gatsby-source-prismic version: 3.2.1

Related

Variable "$slug" of required type "String!" was not provided only when building yet not when developing

I am deploying a Gatsby site using mdx files. When I use npm run develop, my site works as expected. When I use npm run build, I encounter the following error:
Variable "$slug" of required type "String!" was not provided.
It points to my blog.js file (Url path: /templates/blog/) as the source of the error. After some imports, blog.js appears as follows:
export const query = graphql`
query (
$slug: String!
) {
mdx(
fields: {
slug: {
eq: $slug
}
}
) {
frontmatter {
title
details
date(formatString: "LL")
tags
}
body
}
}`
export default function Blog(props) {
return (...content...
)
}
The relevant parts of my config file is as you would expect.
module.exports = {
siteMetadata: {
title: '...',
author: '...'
},
plugins: [...,
{resolve: 'gatsby-source-filesystem',
options: {
name:'src',
path: `${__dirname}/src/`
}
...
My gatsby-node.js is as follows:
/* pathing via node.js; for path.basename*/
const path = require('path')
/* node function that runs when node is created*/
module.exports.onCreateNode = ({node, actions}) => {
const {createNodeField} = actions
if (node.internal.type === `Mdx`) {
const slug = path.basename(node.fileAbsolutePath, '.mdx')
createNodeField({
node,
name: 'slug',
value: slug
})
}
}
// Creating blog pages
// 1.Get path to template
// 2.Get markdown data
// 3.create new pages
module.exports.createPages = async ({graphql, actions}) => {
const{ createPage } = actions
const blogTemplate = path.resolve('./src/templates/blog.js')
const res = await graphql(`
query {
allMdx {
edges {
node {
fields {
slug
}
}
}
}
}
`)
res.data.allMdx.edges.forEach((edge) => {
createPage({
component: blogTemplate,
path: `/blog/${edge.node.fields.slug}`,
context: {
slug: edge.node.fields.slug
}
})
})
}
I'm quite perplexed, as I can open graphQL and find exactly what I would expect at each stage. What am I doing wrong for the build process to fail yet develop process to work?
It's an odd issue, and it would need a careful debug to know what is happening.
My guess is that in some post, the slug is not properly defined, so it's not matching the String! condition, which means that the slug will be a string (everything ok until here) but non-nullable (because of the exclamation mark, !. Further reference here) so it's breaking your GraphQL query.
Try using it as a nullable field:
export const query = graphql`
query (
$slug: String
) {
mdx(
fields: {
slug: {
eq: $slug
}
}
) {
frontmatter {
title
details
date(formatString: "LL")
tags
}
body
}
}`
Your gatsby-node.js looks good so far.
As I mentioned, the ideal solution would be debugging each post to know if there's any invalid slug (adding a debugger or a console.log and checking its values in the terminal's console).

Problem with CreatePage API using gatsby and contentful(Not shown up the content )

I am trying to generate pages of each of the category's data from contentful ...
but there have some concerns.
Success
I could have got the data from contentful ..such as (title, image, and so on ) on the home page(localhost:8000)
Fail
when I clicked the read more button, Card on (localhost:8000), and switched into another page (localhost:8000/blog/tech/(content that I created on contentful)) that has not succeeded and 404pages has appeared(the only slag has changed and appeared that I hoped to).
Error
warn The GraphQL query in the non-page component "C:/Users/taiga/Github/Gatsby-new-development-blog/my-blog/src/templates/blog.js" will not
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.
If the failing component(s) is a regular component and not intended to be a page
component, you generally want to use a <StaticQuery> (https://gatsbyjs.org/docs/static-query)
instead of exporting a page query.
my-repo
enter link description here
gatsby.node.js
const path = require(`path`);
const makeRequest = (graphql, request) => new Promise((resolve, reject) => {
// Query for nodes to use in creating pages.
resolve(
graphql(request).then(result => {
if (result.errors) {
reject(result.errors)
}
return result;
})
)
});
// Implement the Gatsby API "createPages". This is called once the
// data layer is bootstrapped to let plugins create pages from data.
exports.createPages = ({ actions, graphql }) => {
const { createPage } = actions;
// Create pages for each blog.
const getBlog = makeRequest(graphql, `
{
allContentfulBlog (
sort: { fields: [createdAt], order: DESC }
)
edges {
node {
id
slug
}
}
}
}
`).then(result => {
result.data.allContentfulBlog.edges.forEach(({ node }) => {
createPage({
path: `blog/${node.slug}`,
component: path.resolve(`src/templates/blog.js`),
context: {
id: node.id,
},
})
})
});
const getTech = makeRequest(graphql,`
{
allContentfulBlog (
sort: { fields: [createdAt], order: DESC }
filter: {
categories: {elemMatch: {category: {eq: "tech"}}}
},)
{
edges {
node {
id
slug
}
}
}
}
`).then(result => {
const blogs = result.data.allContentfulBlog.edges
const blogsPerPage = 9
const numPages = Math.ceil(blogs.length / blogsPerPage)
Array.from({ length: numPages }).forEach((_, i) => {
createPage({
path: i === 0 ? `/category/tech` : `/category/tech/${i + 1}`,
component: path.resolve("./src/templates/tech.js"),
context: {
limit: blogsPerPage,
skip: i * blogsPerPage,
numPages,
currentPage: i + 1
},
})
})
});
return Promise.all([
getBlog,
getTech
])
};

Fragment cannot be spread here as objects of type "X" can never be of type "Y"

In this case, type "X" is Application and type "Y" is type "Node" - I can see why this is happening, but my understanding of Relay isn't enough to understand how to fix it. The query generated by Relay is
query {
node(id: $some_id) {
...F0
}
}
fragment F0 on Application {
...
}
I have a schema that looks like
query {
application {
/* kind of a generic endpoint for fetching lists, etc */
invites(token: $token) {
name
}
}
viewer { /* the current user */ }
}
I'm trying to fetch a specific invite from outside a session (viewer is null).
I've tried
const application = Relay.QL`query { application }`
...
<Route ... queries={{ application }}/>
...
Relay.createContainer(Component, {
initialValues: { token: null },
fragments: {
application: () => {
fragment on Application {
invites(token: $token) {
...
}
}
}
}
})
which gives me the error
fragment "F0" cannot be spread here as objects of type "Node" can never be of type "Application" - or something to that effect.
I'm a little confused, because if I were to write a raw query and run it through GraphQL directly
query {
application {
invites(token: "asdasdasd") {
edges {
node {
name
}
}
}
}
}
it gives me what I'm looking for...
In the backend, my graph is defined like
export const Application = new GraphQLObjectType({
name: 'Application',
fields: () => ({
id: {
type: GraphQLString,
resolve: () => 'APPLICATION_ID'
},
invites: {
type: InviteConnectionType,
args: connectionArgs,
resolve: (application, args) => {
...
}
}
})
})
export default new GraphQLSchema({
query: new GraphQLObjectType({
name: 'query',
fields: {
node: nodeField,
application: {
type: Application,
resolve: (root, args, ctx) => {
return Promise.resolve({})
}
}
}
})
I've been looking at questions like this and some issues on the Relay github, but it's not clear to me how I should implement nodeInterface.
edit: the short-long of the current nodeInterface code is
export const {
nodeInterface,
nodeField
} = nodeDefinitions(
(globalId) => {
const { type, id } = fromGlobalId(globalId)
return db[type].findById(id)
},
(obj) => {
const name = obj.$modelOptions.name.singular
return types[name]
}
)
Application is not a db model, however, just a generic interface to fetch data through. I've tried checking to see if type === 'Application', and returning null (although I see why that doesn't work), returning Application (the GraphQLObject), but that doesn't work... not really sure where to go from there.
You need to automatically generate an unique global id for a GraphQL
type that you want to refetch.
In nodeInterface you tell GraphQL
how to map the id to the corresponding GraphQL object.
By the given server-side object nodeInterface identifies the GraphQL type.
Below is simplified example how it may look like with Application:
// nodeInterface.
var {nodeInterface, nodeField} = nodeDefinitions(
(globalId) => {
var {type, id} = fromGlobalId(globalId);
// The mapping from globalId to actual object id and type.
console.log('globalId:', id);
console.log('type:', type);
if (type === 'Application') {
// getApplication is your db method to retrieve Application object.
// With id you could also retrieve a specific db object.
return getApplication();
} else {
return null;
}
},
(obj) => {
// Note that instanceof does an identity check on the prototype object, so it can be easily fooled.
if (obj instanceof Application) {
return ApplicationType;
} else {
return null;
}
},
);
// Application.
export const ApplicationType = new GraphQLObjectType({
name: 'Application',
fields: () => ({
// Auto-generated, globally defined id.
id: globalIdField('Application'),
_id: {
type: GraphQLString,
resolve: () => 'APPLICATION_ID'
},
invites: {
type: InviteConnectionType,
args: connectionArgs,
resolve: (application, args) => {
...
}
}
}),
// Declaring nodeInterface.
interfaces: [nodeInterface]
});
Note that during the initial fetch nodeInterface is not even executed, so if nodeInterface is returning nothing there won’t be errors at the initial fetch. If that doesn’t make sense or you’re still struggling you can post a link to the repo, I’ll look into it.
To give an update on this, I was on the right path.
The current nodeDefinitions I had just needed a little extra:
nodeDefinitions(
(globalId) => {
const { type, id } = fromGlobalId(globalId)
if (type === 'Application') {
return Promise.resolve(Application)
}
return db[type].findById(id)
},
(obj) => {
if (obj.$modelOptions) {
/* sequelize object */
const name = obj.$modelOptions.name.singular
return types[name]
} else if (obj.name === 'Application') {
return Application
}
return null
}
)
I'm not sure if this is the best way to do it, but it seems to do the trick. The gist is that, if the type of node that I want to be returned is Application, I return the GraphQL object - { ... name: "Application" ... } we'll use the name field from this in the next step (the second callback in nodeDefinitions) to just re-return Application. I think you could return a "custom" object or something else - it doesn't matter so long as you return something unique that you can define a mapping from to a GraphQLObject type (required for the second callback).

Apollo client specific updatequery for all items?

I have two models with the following relation:
Groups has many
Links
So basically my initial query is getAllGroups, which fetches all groups and its links.
I have a mutation which simply creates a link, what is the most efficient way to update my UI?
Currently I have:
export const withCreateLink = graphql(createLink, {
props({ownProps, mutate}) {
return {
createLink(url, description, group) {
return mutate({
variables: {
url,
description,
group
},
updateQueries: {
getAllGroups: (prev, {mutationResult}) => {
const newLink = mutationResult.data.createLink;
return update(prev, {
allGroups: {
links: {
$push: [newLink]
}
}
})
}
}
})
}
}
}
});
Basically I only want to fetch the group which has the link added to it, but I need to specify which group that is. How do I go about this?
I already figured it out, while creating other mutations this way i've found some different approaches to tackle this:
Option 1:
If you have access to the index of the current model that's in your cache, you can pass it as a prop and then use it inside of your query to target the one that it has been added to:
updateQueries: {
getAllGroups: (prev, {mutationResult}) => {
const newLink = mutationResult.data.createLink;
return update(prev, {
allGroups: {
[groupIndex]:
links: {
$push: [newLink]
}
}
})
}
}
Option 2: Using update instead of updateQueries, so you can read the result of the root query, map over it and then append the newly inserted data. After this you can write it to the cache.
update: (proxy, mutationResult) => {
const query = getAllGroups;
const data = proxy.readQuery({query});
data.allGroups.map((groupData) => {
if(groupData.id == group)
groupData.links.push(mutationResult.data.createLink);
});
proxy.writeQuery({
query,
data
})
}

How to put post date in Gatsby URL?

All the Gatsby starter demos have a path like /gatsby-starter-blog/hi-folks/
How do I set it up with /2015-05-28/hi-folks/ or just the year with /2015/hi-folks/.
Thanks!
Two options:
1) Just put the blog posts in directories named like you want the url to be so in this case /2015-05-28/hi-folks/index.md.
2) You can programmatically set paths by exporting a function from gatsby-node.js called rewritePath. It is called for each page with filesystem data for the file the page comes from + the page's metadata. So say you want to set the date of the post in your markdown's frontmatter and have each post be a simple markdown file with paths like /a-great-blog-post.md
So to do what you want, add to your gatsby-node.js something like:
import moment from 'moment'
exports.rewritePath = (parsedFilePath, metadata) => {
if (parsedFilePath.ext === "md") {
return `/${moment(metadata.createdAt).format('YYYY')}/${parsedFilePath.name}/`
}
}
rewritePath is no longer supported in Gatsby. Here's a solution confirmed to works in Gatsby 2.3,
const m = moment(node.frontmatter.date)
const value = `${m.format('YYYY')}/${m.format('MM')}/${slug}`
createNodeField({ node, name: 'slug', value })
In my gatsby-node.js file I added a slug generator and a template resolver.
Inside src/posts I added a folder 2020 inside that folder I create folders that make slug address paths like so my-blog-post and inside those folders, I have an index.md file.
Now I have URL's that look like http://www.example.com/2020/my-blog-post/.
/**
* Implement Gatsby's Node APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/node-apis/
*/
const { createFilePath } = require(`gatsby-source-filesystem`);
// Generates pages for the blog posts
exports.createPages = async function ({ actions, graphql }) {
const { data } = await graphql(`
query {
allMarkdownRemark {
edges {
node {
fields {
slug
}
}
}
}
}
`);
data.allMarkdownRemark.edges.forEach((edge) => {
const slug = edge.node.fields.slug;
actions.createPage({
path: slug,
component: require.resolve(`./src/templates/blog-post.js`),
context: { slug: slug },
});
});
};
// Adds url slugs to the graph data response
exports.onCreateNode = ({ node, getNode, actions }) => {
const { createNodeField } = actions;
if (node.internal.type === `MarkdownRemark`) {
const slug = createFilePath({ node, getNode, basePath: `posts` });
createNodeField({
node,
name: `slug`,
value: slug,
});
}
};
Inside my src/templates/blog-post.js file I have:
import React from 'react';
import { graphql } from 'gatsby';
import Layout from '../components/layout';
export default function BlogPost({ data }) {
const post = data.markdownRemark;
const tags = (post.frontmatter.tags || []).map((tag, i) => {
return <li key={i}>#{tag}</li>;
});
return (
<Layout>
<article className="post">
<header>
<h1>{post.frontmatter.title}</h1>
<div className="meta">
<time>{post.frontmatter.date}</time>
<ul>{tags}</ul>
</div>
</header>
<section dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
</Layout>
);
}
export const query = graphql`
query($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
frontmatter {
title
date(formatString: "DD-MMMM-YYYY")
tags
}
}
}
`;
My markdown files then use frontmatter data like so:
---
title: Post title here
date: "2020-05-25T14:23:23Z"
description: "A small intro here"
tags: ["tag1", "tag2"]
---
Main post content would be here...

Resources