How to define Gatsby markdown page paths in one file? - graphql

Bu default, Gatsby uses frontmatter for defining paths, like:
---
path: /example-page
---
and then works with it via GraphQL.
What is the best way to define those paths for all markdown files by not writing frontmatter part in every file, but in one file, as an instance, like this:
[
{
"title": "Example",
"path": "/example-page.md"
}
]

You can do that by adding the path when the page is created.
Add this in gatsby-node :
const { createFilePath } = require(`gatsby-source-filesystem`);
exports.onCreateNode = ({ node, getNode, boundActionCreators }) => {
const { createNodeField } = boundActionCreators
if (node.internal.type === `MarkdownRemark`) {
const slug = createFilePath({
node,
getNode,
basePath: `pages`
})
createNodeField({
node,
name: `slug`,
value: `/pages${slug}`
})
}
};
createFilePath turn markdown files in pages directory into /pages/slug.
createNodeField creates new query'able field with name of 'slug'.
Now in graphql you can access the slug :
{
allMarkdownRemark {
edges {
node {
fields {
slug
}
}
}
}
}
Then you can create your pages as usual using the new slug field as page path.
With that you can add your title and all you want in data accessible in graphql.
Example here : https://www.gatsbyjs.org/tutorial/part-seven/

Related

Dynamically create pages with Gatsby based on many Contentful references

I am currently using Gatsby's collection routes API to create pages for a simple blog with data coming from Contentful.
For example, creating a page for each blogpost category :
-- src/pages/categories/{contentfulBlogPost.category}.js
export const query = graphql`
query categoriesQuery($category: String = "") {
allContentfulBlogPost(filter: { category: { eq: $category } }) {
edges {
node {
title
category
description {
description
}
...
}
}
}
}
...
[React component mapping all blogposts from each category in a list]
...
This is working fine.
But now I would like to have multiple categories per blogpost, so I switched to Contentful's references, many content-type, which allows to have multiple entries for a field :
Now the result of my graphQL query on field category2 is an array of different categories for each blogpost :
Query :
query categoriesQuery {
allContentfulBlogPost {
edges {
node {
category2 {
id
name
slug
}
}
}
}
}
Output :
{
"data": {
"allContentfulBlogPost": {
"edges": [
{
"node": {
"category2": [
{
"id": "75b89e48-a8c9-54fd-9742-cdf70c416b0e",
"name": "Test",
"slug": "test"
},
{
"id": "568r9e48-t1i8-sx4t8-9742-cdf70c4ed789vtu",
"name": "Test2",
"slug": "test-2"
}
]
}
},
{
"node": {
"category2": [
{
"id": "75b89e48-a8c9-54fd-9742-cdf70c416b0e",
"name": "Test",
"slug": "test"
}
]
}
},
...
Now that categories are inside an array, I don't know how to :
write a query variable to filter categories names ;
use the slug field as a route to dynamically create the page.
For blogposts authors I was doing :
query authorsQuery($author__slug: String = "") {
allContentfulBlogPost(filter: { author: { slug: { eq: $author__slug } } }) {
edges {
node {
id
author {
slug
name
}
...
}
...
}
And creating pages with src/pages/authors/{contentfulBlogPost.author__slug}.js
I guess I'll have to use the createPages API instead.
You can achieve the result using the Filesystem API, something like this may work:
src/pages/category/{contentfulBlogPost.category2__name}.js
In this case, it seems that this approach may lead to some caveats, since you may potentially create duplicated pages with the same URL (slug) because the posts can contain multiple and repeated categories.
However, I think it's more succinct to use the createPages API as you said, keeping in mind that you will need to treat the categories to avoid duplicities because they are in a one-to-many relationship.
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const result = await graphql(`
query {
allContentfulBlogPost {
edges {
node {
category2 {
id
name
slug
}
}
}
}
}
`)
let categories= { slugs: [], names: [] };
result.data.allContentfulBlogPost.edges.map(({node}))=> {
let { name, slug } = node.category2;
// make some checks if needed here
categories.slugs.push(slug);
categories.names.push(name);
return new Set(categories.slugs) && new Set(categories.names);
});
categories.slugs.forEach((category, index) => {
let name = categories.names[index];
createPage({
path: `category/${category}`,
component: path.resolve(`./src/templates/your-category-template.js`),
context: {
name
}
});
});
}
The code's quite self-explanatory. Basically you are defining an empty object (categories) that contains two arrays, slugs and names:
let categories= { slugs: [], names: [] };
After that, you only need to loop through the result of the query (result) and push the field values (name, slug, and others if needed) to the previous array, making the needed checks if you want (to avoid pushing empty values, or that matches some regular expression, etc) and return a new Set to remove the duplicates.
Then, you only need to loop through the slugs to create pages using createPage API and pass the needed data via context:
context: {
name
}
Because of redundancy, this is the same than doing:
context: {
name: name
}
So, in your template, you will get the name in pageContext props. Replace it with the slug if needed, depending on your situation and your use case, the approach is exactly the same.

Gatsby warning on build "query takes too long"

I am building a static website with Gatsby and Netlify CMS. The website is hosted on Netlify as well. I have a blog section and I generate a page per article from a markdown file. I have the following warning during build "query takes too long" for every article page I generate. The site builds eventually but the build time gets longer and longer the more pages I am generating so I'm afraid it will become too long when I start having too many articles in my site.
I am generating a page for each markdown file that is being created by netlify CMS.
Would you mind looking and the code I have in the gatsby-node file and the query I am using in my blog template file to see if I'm doing anything wrong that would explain the build time warning message ?
Thanks
Here is my development environment
npmPackages:
gatsby: ^2.26.1 => 2.26.1
gatsby-image: ^2.10.0 => 2.10.0
gatsby-plugin-netlify-cms: ^4.8.0 => 4.8.0
gatsby-plugin-react-helmet: ^3.8.0 => 3.8.0
gatsby-plugin-sharp: ^2.13.0 => 2.13.0
gatsby-plugin-styled-components: ^3.9.0 => 3.9.0
gatsby-remark-images: ^3.10.0 => 3.10.0
gatsby-remark-prismjs: ^3.12.0 => 3.12.0
gatsby-source-filesystem: ^2.9.1 => 2.9.1
gatsby-transformer-remark: ^2.15.0 => 2.15.0
gatsby-transformer-sharp: ^2.11.0 => 2.11.0
npmGlobalPackages:
gatsby-cli: 2.18.0
here is the code I've got in the gatsby-node file to generate my posts pages
exports.createPages = async ({ actions, graphql, reporter }) => {
const { createPage } = actions
const blogPostTemplate = require.resolve(`./src/templates/blog-post.js`)
const categoryPageTemplate = require.resolve(
`./src/templates/category-page.js`
)
const uncategorizedPageTemplate = require.resolve(
`./src/templates/uncategorized.js`
)
const _ = require("lodash")
const result = await graphql(`
{
posts: allMarkdownRemark(
sort: { fields: [frontmatter___date], order: DESC }
) {
edges {
node {
id
frontmatter {
categories
}
fields {
slug
}
}
}
}
categoriesGroup: allMarkdownRemark {
group(field: frontmatter___categories) {
fieldValue
}
}
}
`)
// Handle errors
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL query.`)
return
}
const posts = result.data.posts.edges
const categories = result.data.categoriesGroup.group
posts.forEach(({ node }, index) => {
const nextPostId = index === 0 ? null : posts[index - 1].node.id
const previousPostId =
index === posts.length - 1 ? null : posts[index + 1].node.id
createPage({
path: `blog${node.fields.slug}`,
component: blogPostTemplate,
context: {
// additional data can be passed via context
id: node.id,
index,
nextPostId
previousPostId
},
})
})
}
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({
node,
name: `slug`,
value,
})
}
}
and here is the query I've got in my blog-post template file to get the post with the id from the pageContext:
export const pageQuery = graphql`
query($id: String!, $previousPostId: String, $nextPostId: String) {
markdownRemark(id: { eq: $id }) {
id
html
frontmatter {
featuredImage {
childImageSharp {
fluid(maxWidth: 1600) {
...GatsbyImageSharpFluid_withWebp_tracedSVG
}
}
}
title
description
date(formatString: "MMMM DD, YYYY")
categories
}
}
previous: markdownRemark(id: { eq: $previousPostId }) {
frontmatter {
title
}
fields {
slug
}
}
next: markdownRemark(id: { eq: $nextPostId }) {
frontmatter {
title
}
fields {
slug
}
}
}
`
Gatsby's team is actually working on reducing the build time by adding some "cache" features. You can follow the stack trace in their releases notes, they are still in beta testing (some of them are focused mainly on gatsby develop.
If you want to try it them to check if it improves the build-develop time, you just need to upgrade Gatsby to the latest version (^2.28) and:
// In your gatsby-config.js
module.exports = {
// your existing config
flags: {
FAST_DEV: true,
},
}
Regarding the gatsby build, in the meantime, in Netlify, you can activate some plugins (Gatsby Cache for example).
Among all this stuff, you can also add the incremental build feature (described in this Netlify's post by the great Jason Lengstorf). After you've installed the needed dependencies (upgrade Gatsby and cross-env), just customize the build command (also in Netlify's dashboard) to enable the PAGE_BUILD_ON_DATA feature:
"build": "cross-env GATSBY_EXPERIMENTAL_PAGE_BUILD_ON_DATA_CHANGES=true gatsby build --log-pages"

Using frontmatter in a markdownfile to query for images in a page query

I'm using GraphQL from within a Gatsby project. I have a set of markdown files for a blog-like section of the site. In the frontmatter of each markdown file, there's an image property.
What I'd like to do is use Gatsby's fine image api to load the image in the frontmatter. When viewing an individual post (the ones created via createPage api), this works just fine because I can provide the frontmatter.image in the context. Here's what that query looks like.
export const pageQuery = graphql`
query($slug: String!, $image: String) {
markdownRemark(frontmatter: { slug: { eq: $slug } }) {
html
frontmatter {
date(formatString: "MMMM DD, YYYY")
slug
title
image
}
}
coverImage: file(relativePath: { eq: $image }) {
childImageSharp {
fluid(maxWidth: 1440) {
...GatsbyImageSharpFluid
}
}
}
}
`
On my index page where I'm displaying all these posts though, I want to display a smaller version of this image. I can get the image from the front matter easy enough, but I'm not sure how to integrate that into the query.
export const pageQuery = graphql`
query {
allMarkdownRemark(sort: { order: DESC, fields: [frontmatter___date] }) {
edges {
node {
id
excerpt(pruneLength: 250)
frontmatter {
date(formatString: "MMMM DD, YYYY")
slug
title
image # <--- want to use this in a file query
}
}
}
}
}
`
As far as I understand, I can't use string interpolation in a static query in the component where the image is actually used, so I need to get it here in the page query. Is what I'm trying to do possible? Is there a better way to handle this?
This "link" between your frontmatter's image string and an actual image file node (processed with Sharp) is called a foreign-key relationship.
Creating foreign-key relationships in Gatsby
There are two ways of doing this:
Using mappings in gatsby-config.js
Using a GraphQL #link directive through Gatsby's schema customization (from v2.2)
I recommend the second option, since it's a more GraphQL way of doing things, and happens in gatsby-node.js where most node operations are taking place. However, if you're starting out with Gatsby and GraphQL, the first option might be easier to set up.
Implementing the #link directive in gatsby-node.js
In your case, using the #link GraphQL directive, you would probably end up with something like this in your gatsby-node.js:
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
const typeDefs = [
`type MarkdownRemark implements Node { frontmatter: Frontmatter }`,
`type Frontmatter {
# you may need to adapt this line depending on the node type and key
# that you want to create the relationship for
image: File #link(by: "relativePath")
}`
]
createTypes(typeDefs)
}
If you want to see an example in the wild, check out gatsby-node.js in robinmetral/eaudepoisson.com.
Query processed images via your frontmatter
Finally, you'll be able to query like this:
{
allMarkdownRemark {
edges {
node {
frontmatter {
date
slug
title
# image now points to the image file node
image {
childImageSharp {
fluid(maxWidth: 1024) {
...GatsbyImageSharpFluid
}
}
}
}
}
}
}
}

Use absolute path for featured image in markdown post with Gatsby

I've followed Gatsby tutorial for Working With Images in Markdown Posts and Pages which is working well but what I want to achieve is to fetch image from a static location instead of using a relative path for the image.
Would like to reference image like this (in frontmatter)
featuredImage: img/IMG_20190621_112048_2.jpg
Where IMG_20190621_112048_2.jpg is in /src/data/img instead of same directory as markdown file under /src/posts
I've tried to setup gatsby-source-filesystem like this :
{
resolve: `gatsby-source-filesystem`,
options: {
name: `posts`,
path: `${__dirname}/src/posts`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `data`,
path: `${__dirname}/src/data/`,
},
},
but graphQL query in post template fails :
export const query = graphql`
query($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
frontmatter {
title
featuredImage {
childImageSharp {
fluid(maxWidth: 800) {
...GatsbyImageSharpFluid
}
}
}
}
}
}
GraphQL Error Field "featuredImage" must not have a selection since
type "String" has no subfields.
Any idea how I could fetch image from a location distinct to the post markdown directory ?
Achieving this in Gatsby used to be pretty troublesome, but thanks to the new createSchemaCustomization Node API docs (since Gatsby 2.5) it's relatively easy.
Here's a demo where I replicate your repo structure: github
Here's where the relevant code lives: github
Here's the code to make it work:
// gatsby-node.js
const path = require('path')
exports.createSchemaCustomization = ({ actions }) => {
const { createFieldExtension, createTypes } = actions
createFieldExtension({
name: 'fileByDataPath',
extend: () => ({
resolve: function (src, args, context, info) {
const partialPath = src.featureImage
if (!partialPath) {
return null
}
const filePath = path.join(__dirname, 'src/data', partialPath)
const fileNode = context.nodeModel.runQuery({
firstOnly: true,
type: 'File',
query: {
filter: {
absolutePath: {
eq: filePath
}
}
}
})
if (!fileNode) {
return null
}
return fileNode
}
})
})
const typeDefs = `
type Frontmatter #infer {
featureImage: File #fileByDataPath
}
type MarkdownRemark implements Node #infer {
frontmatter: Frontmatter
}
`
createTypes(typeDefs)
}
How it works:
There are 2 parts to this:
Extend markdownRemark.frontmatter.featureImage so graphql resolves to a File node instead of a string via createTypes
Create a new field extension #fileByDataPath via createFieldExtension
createTypes
Right now Gatsby's inferring frontmatter.featureImage as a string. We'll ask Gatsby to read featureImage as a string instead, by modifying its parent type:
type Frontmatter {
featureImage: File
}
This is not enough however, we'll also need to pass this Frontmatter type to its parent as well:
type Frontmatter {
featureImage: File
}
type MarkdownRemark implements Node {
frontmatter: Frontmatter
}
We'll also add the #infer tag, which lets Gatsby know that it can infer other fields of these types, i.e frontmatter.title, markdownRemark.html, etc.
Then pass these custom type to createTypes:
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type Frontmatter #infer {
featureImage: File
}
type MarkdownRemark implements Node #infer {
frontmatter: Frontmatter
}
`
createTypes(typeDefs)
}
Now, we can fire up localhost:8000/___graphql and try to query the image
query Post {
markdownRemark {
frontmatter {
featureImage {
id
}
}
}
}
and we get...
Error: Cannot return null for non-nullable field File.id.
That is because while Gatsby now understands featureImage should be a File node, it has no idea where to get that file.
At this point, we can either use createResolvers to manually resolve the field to a File node, or createFileExtension to do the same thing. I choose createFileExtension because it allows more code reuse (you can extend any fields), while createResolvers, in this case, is more useful for a specific field. Seeing that all you want is to resolve a file from the src/data directory, I'll call this extension fieldByDataPath.
createFileExtension
Let's just look at the resolve attribute. It is a function that takes in the following:
source: The data of the parent field (in this case, frontmatter)
args: The arguments passed to featureImage in a query. We won't need this
context: contains nodeModel, which we'll use to get nodes from Gatsby node store
info: metadata about this field + the whole schema
We will find the original path (img/photo.jpg) from src.featureImage, then glue it to src/data to get a complete absolute path. Next, we query the nodeModel to find a File node with the matching absolute path. Since you have already pointed gatsby-source-filesystem to src/data, the image (photo.jpg) will be in Gatsby node store.
In case we can't find a path or a matching node, return null.
resolve: async function (src, args, context) {
// look up original string, i.e img/photo.jpg
const partialPath = src.featureImage
if (!partialPath) {
return null
}
// get the absolute path of the image file in the filesystem
const filePath = path.join(__dirname, 'src/data', partialPath)
// look for a node with matching path
const fileNode = await context.nodeModel.runQuery({
firstOnly: true,
type: 'File',
query: {
filter: {
absolutePath: {
eq: filePath
}
}
}
})
// no node? return
if (!fileNode) {
return null
}
// else return the node
return fileNode
}
We've done 99% of the work. The last thing to do is to move this to pass this resolve function to createFieldExtension; as well as add the new extension to createTypes
createFieldExtension({
name: 'fileByDataPath' // we'll use it in createTypes as `#fileByDataPath`
extend: () => ({
resolve, // the resolve function above
})
})
const typeDef = `
type Frontmatter #infer {
featureImage: File #fileByDataPath // <---
}
...
`
With that, you can now use relative path from src/data/ in frontmatter.
Extra
The way fileByDataPath implemented, it'll only work with fields named featureImage. That's not too useful, so we should modify it so that it'll work on any field that, say, whose name ended in _data; or at the very least accept a list of field names to work on.
Edit had a bit of time on my hand, so I wrote a plugin that does this & also wrote a blog on it.
Edit 2 Gatsby has since made runQuery asynchronous (Jul 2020), updated the answer to reflect this.
In addition to Derek Answer which allow assets of any type to be use anywhere (sound, video, gpx, ...), if looking for a solution only for images, one can use :
https://www.gatsbyjs.org/packages/gatsby-remark-relative-images/
The reason in your server schema you may have declared the featuredImage variable as string and in your client graphql query you are trying to call subobjects of the featuredImage variable and that subobjects is not existing.
You may have to check the graphql schema definition and align the query with the schema definition
you current schema might be like this
featuredImage: String
and you need to change it by declaring the proper types based on the requirements in the server side.
For more information about graphql types. please refer this url - https://graphql.org/learn/schema/#object-types-and-fields
Thanks
Rigin Oommen

How to get 'Last Update Date' of a blog post in GATSBY.js

Hello I'm not a dev so may the question will be easy for you guys. I used the advance starter from gatsby site. The blog is working perfect but I need to provide the LAST UPDATED time under my title. Searched for some solutions but none of them worked. Could you Provide some help?
gatsby-node.js
exports.onCreateNode = ({ node, getNode, actions }) => {
const { createNodeField } = actions;
if (node.internal.type !== 'MarkdownRemark') {
return;
}
const fileNode = getNode(node.parent);
createNodeField({
node,
name: 'modifiedTime',
value: fileNode.mtime
});
};
`````````````````````````
PostListing.jsx
class PostListing extends React.Component {
getPostList() {
const postList = [];
this.props.postEdges.forEach(postEdge => {
postList.push({
path: postEdge.node.fields.slug,
tags: postEdge.node.frontmatter.tags,
cover: postEdge.node.frontmatter.cover,
title: postEdge.node.frontmatter.title,
date: postEdge.node.fields.date,
excerpt: postEdge.node.excerpt,
timeToRead: postEdge.node.timeToRead,
modifiedTime:postEdge.node.modifiedTime
});
});
return postList;
}
render() {
const postList = this.getPostList();
return (
<div className='posts'>
{/* Your post list here. */
postList.map(post => (
<Fragment>
<div className='singlePost__date'>
<h4 style={{color:'white'}}> {post.modifiedTime}</h4>
</div>
<div className='singlePost__Title'>
<Link classname='singlePost' to={post.path} key={post.title}>
<h1 className='singlePost__title'>{post.title}</h1>
</Link>
</div>
</Fragment>
))}
</div>
);
}
}
export default PostListing;
I expect something like
TITLE
last updated : 3/2/2019
You can use information stored in Git to get the latest time when a file was modified.
1st approach
Track it manually, but this can be error-prone if you forget to edit the modified time. So I would recommend that as the last option if you can't get others to work.
2nd approach
You can edit your gatsby-node.js to pull information from Git like so:
const { execSync } = require("child_process")
exports.onCreateNode = ({ node, actions }) => {
// ...
if (node.internal.type === "MarkdownRemark") {
const gitAuthorTime = execSync(
`git log -1 --pretty=format:%aI ${node.fileAbsolutePath}`
).toString()
actions.createNodeField({
node,
name: "gitAuthorTime",
value: gitAuthorTime,
})
}
// ...
}
Then, in your template, you can fetch it:
query($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
# ...
fields {
gitAuthorTime
}
# ...
}
}
And, finally, use it in JSX like so:
import React from "react"
const BlogPost = (props) => {
const { gitAuthorTime } = props.data.markdownRemark.fields
render(<p>Updated at: ${gitAuthorTime}</p>)
}
export default BlogPost
3rd approach
Similar to the previous one but it uses a plugin gatsby-transformer-info. It does a similar thing as in the 2nd approach, but you need to access the modified time differently this time. Like so:
query($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
# ...
parent {
... on File {
fields {
gitLogLatestDate
}
}
}
# ...
}
}
I wrote more about this in my blog post "Add Updated At To Your Gatsby Blog" if you want to check it out.
Edit: The answer below is actually wrong, since File. modifiedTime is the modifiedTime of the markdown file itself & not the modifiedTime for your content. For example, if you deploy your blog on say, Netlify, then the modifiedTime of your files there will be different than in your local environment.
I think the right answer is to track it separately. If you're using a CMS like NetlifyCMS, you can create a field that automatically update the date/time on every edit.
Wherever you're querying for your markdown files, you can use the below field:
query {
allMarkdownRemark {
edges {
node {
frontmatter { /* other stuff */ }
parent {
... on File {
modifiedTime(formatString: "MM/DD/YYYY")
}
}
}
}
}
}
And access it in your via postEdge.node.parent.modifiedTime

Resources