GraphQL: Overriding slugs - javascript

👋
I'm starting my first project with Gatsby and have run into an issue with querying data that "may" not always exist. Here is my gatsby-node.js file:
const path = require('path');
const _ = require('lodash');
// Lifecycle methods
function attachFieldsToBlogPost({ node, actions }) {
if (node.internal.type !== 'MarkdownRemark') {
return;
}
const { createNodeField } = actions;
const { slug, title } = node.frontmatter;
const postPath = slug || _.kebabCase(title);
createNodeField({
node,
name: 'slug',
getter: node => node.frontmatter.slug, // eslint-disable-line no-shadow
value: postPath,
});
createNodeField({
node,
name: 'url',
value: postPath,
});
}
exports.onCreateNode = function() { // eslint-disable-line func-names
return Promise.all([attachFieldsToBlogPost].map(fn => fn.apply(this, arguments))); // eslint-disable-line prefer-rest-params
};
// Implementations
function getMarkdownQuery({ regex } = {}) {
return `
{
allMarkdownRemark(
sort: { fields: [frontmatter___date], order: DESC }
filter: { fileAbsolutePath: { regex: "${regex}" } }
) {
totalCount
edges {
node {
fileAbsolutePath
excerpt(pruneLength: 280)
timeToRead
frontmatter {
title
date
slug
}
fields {
url
slug
}
}
}
}
}
`;
}
function createBlogPostPages({ edges, createPage }) {
const component = path.resolve('src/templates/Post.js');
edges.forEach(({ node }) => {
const { slug, title } = node.frontmatter;
const postPath = slug || _.kebabCase(title);
createPage({
path: postPath,
component,
context: {
slug: postPath,
},
});
});
}
exports.createPages = async({ actions, graphql }) => {
const results = await Promise.all([
graphql(getMarkdownQuery({ regex: '/src/posts/' })),
]);
const error = results.filter(r => r.errors);
if (error.length) {
return Promise.reject(error[0].errors);
}
const [blogPostResults] = results;
const { createPage } = actions;
const blogPostEdges = blogPostResults.data.allMarkdownRemark.edges;
createBlogPostPages({
createPage,
edges: blogPostEdges,
});
};
And my example blog post content is:
---
title: 'Hello world there'
date: '2018-08-25'
---
Here is some content.
```javascript
console.log('test')
```
When I supply a slug frontmatter component, the page is created as intended. But, I'd only like to use the slug parameter when it's available (hence the check to see if the frontmatter is available in both attachFieldsToBlogPost and createBlogPostPages). If I remove the slug frontmatter item, I get the following error:
GraphQLError: Cannot query field "slug" on type "frontmatter".
Is there a way to override the path used to create the post pages "if" the slug frontmatter is there?
Hopefully this isn't too vague as it's my first Gatsby project, but seems like a pretty useful feature. Thanks!

If you remove every case of slug then the field will no longer exist in Gatsby's internal schema. You're probably running into the issue described here.
So long as you have at least 1 markdown file with a slug field, then it will exist in Gatsby's automated GraphQL schema, and then your conditional logic should work.

Related

Adding a 2nd dynamic template to Gatsby/NetlifyCMS correctly - Where am I going wrong?

I've added a blog page that I integrated with NetlifyCMS and it works wonderful. However, I need to have one more dynamic page on my site that displays services and each service will have its own generated template, will pretty much be very similar to the blog page in theory.
So I read around a bunch of different solutions but kept hitting a wall. What am I doing wrong here? and whats the simplest way to make this work, as I wont be adding any other dynamic pages moving forward.
Here was my gatsby-node.js when I just had the blog page and everything worked great:
const { createFilePath } = require("gatsby-source-filesystem")
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({ node, getNode })
createNodeField({
name: `slug`,
node,
value,
})
}
}
//Blog
async function getPageData(graphql) {
return await graphql(`
{
blogPosts: allMarkdownRemark {
edges {
node {
fields {
slug
}
}
}
}
}
`)
}
const path = require(`path`)
exports.createPages = async ({ graphql, actions }) => {
const { data } = await getPageData(graphql)
data.blogPosts.edges.forEach(({ node }) => {
const { slug } = node.fields
actions.createPage({
path: `/articles${slug}`,
component: path.resolve("./src/templates/articlepost.js"),
context: {slug: slug },
})
})
}
So I took a stab it and tried to do the same thing but adding my services page. Here is what that ended up looking like:
gatsby-node.js
const { createFilePath } = require("gatsby-source-filesystem")
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({ node, getNode })
createNodeField({
name: `slug`,
node,
value,
})
}
}
async function getBlogData(graphql) {
return await graphql(`
{
blogPosts: allMarkdownRemark(
filter: { frontmatter: { template: { eq: "articlepost" } } }
) {
edges {
node {
fields {
slug
}
}
}
}
}
`)
}
async function getServiceData(graphql) {
return await graphql(`
{
servicePosts: allMarkdownRemark(
filter: { frontmatter: { template: { eq: "service" } } }
) {
edges {
node {
fields {
slug
}
}
}
}
}
`)
}
const path = require(`path`)
exports.createPages = async ({ graphql, actions }) => {
const blogPage = path.resolve("./src/templates/articlepost.js")
const servicePage = path.resolve("./src/templates/service.js")
const { data } = await getBlogData(graphql)
data.blogPosts.edges.forEach(({ node }) => {
const { slug } = node.fields
actions.createPage({
path: `/articles${slug}`,
component: blogPage,
context: { slug: slug },
})
})
const { serviceData } = await getServiceData(graphql)
serviceData.servicePosts.edges.forEach(({ node }) => {
const { slug } = node.fields
actions.createPage({
path: `/streaming-services${slug}`,
component: servicePage,
context: { slug: slug },
})
})
}
My blog page still works fine but I get error:
As you can see I added a template type to my config.yml, that now shows in my frontmatter to separate my posts, but I think it may have just confused me a little more. Totally fine with removing the template types from frontmatter, but really need a very simple way for this to work.
Many thanks in advance!
Try aliasing your loops to avoid scoped elements.
const { data } = await getBlogData(graphql)
data.blogPosts.edges.forEach(({ node: post }) => {
const { slug } = post.fields
actions.createPage({
path: `/articles${slug}`,
component: blogPage,
context: { slug: slug },
})
})
And:
serviceData.servicePosts.edges.forEach(({ node: service }) => {
const { slug } = service.fields
actions.createPage({
path: `/streaming-services${slug}`,
component: servicePage,
context: { slug: slug },
})
})
Basically, you are aliasing the loop item to make them unique (different from the previous loop) as post and service respectively.
I would recommend reading this article from David Walsh.
const { serviceData } = await getServiceData(graphql)
doesn't work as no serviceData in object returned from graphql( (returned by getServiceData) - it's always a data named property.
You can reuse data (if earlier data not required later) or use alias in destructuring:
const { data: serviceData } = await getServiceData(graphql)

how to create multiple pages using different templates in 'gatsby-node'?

i'm new to gatsby and trying to programatically create pages using different templates, but I'm struggling with that.
I have two different pages (landing & press), both written in mdx files, that need different routes and templates when being created programmatically.
my landing works good, but i failed to add the "press" page. everything i tried didn't work.
for information, i have a templateKey 'press' and 'landing' in each frontmatter of my markdown files.
Here is my gatsby-node file :
const path = require("path");
const { createFilePath } = require("gatsby-source-filesystem");
const { fmImagesToRelative } = require("gatsby-remark-relative-images");
exports.createPages = ({ actions, graphql }) => {
const { createPage } = actions;
const landingTemplate = path.resolve("src/templates/landing.js");
// const pressTemplate = path.resolve("./src/templates/press.js");
return graphql(`
{
allMarkdownRemark(limit: 1000) {
edges {
node {
id
fields {
slug
}
frontmatter {
templateKey
locale
}
}
}
}
}
`).then((result) => {
if (result.errors) {
result.errors.forEach((e) => console.error(e.toString()));
return Promise.reject(result.errors);
}
const posts = result.data.allMarkdownRemark.edges;
posts.forEach((edge) => {
const id = edge.node.id;
createPage({
path: edge.node.fields.slug,
component: landingTemplate,
context: {
locale: edge.node.frontmatter.locale,
id,
},
});
});
});
};
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions;
fmImagesToRelative(node); // convert image paths for gatsby images
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({ node, getNode });
createNodeField({
name: `slug`,
node,
value,
});
}
};
here is my query in graphiQL
Thank you in advance for any assistance that you may be able to give me !
You have to make two different queries to get all your press and all landing data (filtering by each templateKey). Once you have the different populated objects you need to call for each object the createPage API to create pages and pass each template value. It should look like:
const path = require("path");
const { createFilePath } = require("gatsby-source-filesystem");
const { fmImagesToRelative } = require("gatsby-remark-relative-images");
exports.createPages = async ({ actions, graphql }) => {
const { createPage } = actions;
const landingTemplate = path.resolve("src/templates/landing.js");
const pressTemplate = path.resolve("./src/templates/press.js");
const postQuery = await graphql(`
{
allMarkdownRemark(
limit: 1000
filter: { frontmatter: { templateKey: { eq: "press" }}},
) {
edges {
node {
id
fields {
slug
}
frontmatter {
templateKey
locale
}
}
}
}
}
`).then((result) => {
if (result.errors) {
result.errors.forEach((e) => console.error(e.toString()));
return Promise.reject(result.errors);
}
const posts = postQuery.data.allMarkdownRemark.edges;
posts.forEach((edge) => {
const id = edge.node.id;
createPage({
path: edge.node.fields.slug,
component: pressTemplate,
context: {
locale: edge.node.frontmatter.locale,
id,
},
});
});
});
const landingQuery = await graphql(`
{
allMarkdownRemark(
limit: 1000
filter: { frontmatter: { templateKey: { eq: "landing" }}},
) {
edges {
node {
id
fields {
slug
}
frontmatter {
templateKey
locale
}
}
}
}
}
`).then((result) => {
if (result.errors) {
result.errors.forEach((e) => console.error(e.toString()));
return Promise.reject(result.errors);
}
const landings = landingQuery.data.allMarkdownRemark.edges;
landings.forEach((edge) => {
const id = edge.node.id;
createPage({
path: edge.node.fields.slug,
component: landingTemplate,
context: {
locale: edge.node.frontmatter.locale,
id,
},
});
});
});
};
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions;
fmImagesToRelative(node); // convert image paths for gatsby images
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({ node, getNode });
createNodeField({
name: `slug`,
node,
value,
});
}
};
Your mistake was that you were returning your GraphQL query and that limit your capability to create new queries. Note that I've also added a promise-based approach, with an async and await functions in order to optimize the queries and the timings.
Without knowing your data structure I've duplicated the query changing the templateKey value, of course, you will need to customize the data that you want to gather for each template and the data you want to pass via context API.
So I created a method based on a netlify CMS starter, which it looks like you attempted as well, that makes sure you only make the query once:
exports.createPages = async ({graphql,actions }) => {
const { data } = await graphql(`
query Pages {
allMarkdownRemark {
nodes {
frontmatter {
slug
template
}
}
}
}
`)
data.allMarkdownRemark.nodes.forEach(node => {
actions.createPage({
path: `${node.frontmatter.slug}`,
component: path.resolve(
`./src/templates/${String(node.frontmatter.template)}.js`
),
context: {
slug: node.frontmatter.slug,
},
})
})
}
Then in the template it self you can call this query:
export const aboutPageQuery = graphql`
query AboutPage($slug: String) {
markdownRemark(frontmatter: { slug: { eq: $slug } }) {
frontmatter {
title
story {
text
title
}
}
}
}
`
Note: I'm still working through a method to include templates based on path. Say if you had a blog directory within pages that you wanted to apply a single template (blog-page.js) and custom path (/blog/{slug}).

How to add a custom GraphQL parameter in a GatsbyJS node?

I created the following gatsby node to query 1 record
const axios = require("axios");
exports.sourceNodes = async (
{ actions, createNodeId, createContentDigest },
configOptions
) => {
const { createNode } = actions;
// Gatsby adds a configOption that's not needed for this plugin, delete it
delete configOptions.plugins;
// Helper function that processes a post to match Gatsby's node structure
const processPost = post => {
const nodeId = createNodeId(`gutenberg-post-${post.id}`);
const nodeContent = JSON.stringify(post);
const nodeData = Object.assign({}, post, {
id: nodeId,
parent: null,
children: [],
internal: {
type: `GutenbergPost`,
content: nodeContent,
contentDigest: createContentDigest(post)
}
});
return nodeData;
};
const apiUrl = `http://wp.dev/wp-json/gutes-db/v1/${
configOptions.id || 1
}`;
// Gatsby expects sourceNodes to return a promise
return (
// Fetch a response from the apiUrl
axios
.get(apiUrl)
// Process the response data into a node
.then(res => {
// Process the post data to match the structure of a Gatsby node
const nodeData = processPost(res.data);
// Use Gatsby's createNode helper to create a node from the node data
createNode(nodeData);
})
);
};
My source is a rest API that has the following format:
http://wp.dev/wp-json/gutes-db/v1/{ID}
Currently the gatsby node default ID set is 1
I can query it in graphql by doing this:
{
allGutenbergPost {
edges {
node{
data
}
}
}
}
This will always return record 1
I wanted to add a custom parameter for ID so that I could do this
{
allGutenbergPost(id: 2) {
edges {
node{
data
}
}
}
}
What adjustments should I do with my existing code?
I assume you are creating page programmatically? If so, in the onCreatePage hook, when you do createPage, you can pass in a context object. Anything in there will be available as a query variable.
For example, if you have
createPage({
path,
component: blogPostTemplate,
context: {
foo: "bar",
},
})
Then you can do a page query like
export const pageQuery = graphql`
ExampleQuery($foo: String) {
post(name: { eq: $foo }) {
id
content
}
}
`
If you just want to filter by id, you can check out the docs on filter & comparison operators.
{
allGutenbergPost(filter: { id: { eq: 2 }}) {
edges {
node{
data
}
}
}
}
or
{
gutenbergPost(id: { eq: 2 }) {
data
}
}
Hope it helps!

Gatsby - overwriting images

I'm making a site using gatsby CMS, but now I ran into a problem that I have no idea how to solve.
I have project pages that I create from markdown files in gatsby-node. It works as it should, creates pages with proper slugs and so on. Now, the problem is that I have created two project pages, both of them have images that are called the same - but reside in different folders:
In projects/foo.md
---
templateKey: project-page
headerImage: /img/projects/foo/Header.jpg
---
Then in projects/bar.md
---
templateKey: project-page
headerImage: /img/projects/bar/Header.jpg
---
Then in the template I call
<div className="hero"
style={{ backgroundImage: `url(${data.headerImage.publicURL})` }}
>
It works well for foo project. But for bar one it still shows the same image as in foo, for both being style='background-image: url("/static/Header-920eee5baf94303a7a172f4ccef3b60b.jpg");'. I have no idea what is going on.
If it helps, the content of my gatsby-node.js is:
const _ = require('lodash')
const path = require('path')
const { createFilePath } = require('gatsby-source-filesystem')
const { fmImagesToRelative } = require('gatsby-remark-relative-images')
exports.createPages = ({ actions, graphql }) => {
const { createPage } = actions
return graphql(`
{
allMarkdownRemark(limit: 1000) {
edges {
node {
id
fields {
slug
}
frontmatter {
templateKey
headerImage {
publicURL
}
}
}
}
}
}
`).then(result => {
if (result.errors) {
result.errors.forEach(e => console.error(e.toString()))
return Promise.reject(result.errors)
}
const projects = result.data.allMarkdownRemark.edges
projects.forEach(edge => {
const id = edge.node.id
createPage({
path: edge.node.fields.slug,
tags: edge.node.frontmatter.tags,
component: path.resolve(
`src/templates/${String(edge.node.frontmatter.templateKey)}.jsx`
),
// additional data can be passed via context
context: {
data: edge.node.frontmatter
},
})
})
})
}
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
fmImagesToRelative(node) // convert image paths for gatsby images
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({ node, getNode })
createNodeField({
name: `slug`,
node,
value,
})
}
}

Integrate JSON data with publishing process in gatsby-node.js

I've created a function inside gatsby-node.js that creates links between users and locations and publishes them to the correct path based on the info from the markdown files.
Now, I have a local data.json with a survey data for every user that I need to integrate into this function.
I need to import data from the data.json and check - if the currently processed User page has a matching email address in data.json, then add the testimonials to the output data. I'm struggling to find a way of how to integrate this logic into the existing function.
Node.js
const path = require("path");
const { createFilePath } = require("gatsby-source-filesystem");
const data = require("src/data/data.json");
exports.createPages = ({ actions, graphql }) => {
const { createPage } = actions;
return graphql(`
{
allMarkdownRemark(limit: 1000) {
edges {
node {
id
fields {
slug
}
frontmatter {
templateKey
title
location
}
}
}
}
}
`).then(result => {
if (result.errors) {
result.errors.forEach(e => console.error(e.toString()));
return Promise.reject(result.errors);
}
const edges = result.data.allMarkdownRemark.edges;
edges.forEach(edge => {
const id = edge.node.id;
const title = edge.node.frontmatter.title;
const location = edge.node.frontmatter.location || null;
createPage({
path: edge.node.fields.slug,
component: path.resolve(
`src/templates/${String(
edge.node.frontmatter.templateKey,
)}.js`,
),
// query variables are passed in via the context
context: {
id,
title,
location,
},
});
});
});
};
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions;
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({ node, getNode });
createNodeField({
name: `slug`,
node,
value,
});
}
};
JSON
[
{
"agreed": true,
"email": "alex#test.com"
"testimonials": [
{
"name": "Alex",
"city": "Pedro",
"state": "WA",
"description": "Alex was very patient. He is one of the good guys!"
}
]
}
]
You need to include the email field in the graphql query, inside the frontmatter property from your markdown files. From there you can use data.find(u => u.email == edge.node.frontmatter.social_id) to find the right user, and pass the whole object in the comtext of createPage, or just the testimonials, your call.

Categories

Resources