Gatsby dynamic slug

Viewed 1139

First I'm gonna present the code I have and then explain the problem:

I'm having this on my gatsby-node.js:

const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)

exports.onCreateNode = ({ node, getNode, actions }) => {
  const { createNodeField } = actions
  if (node.internal.type === `ContetnfulPost`) {
    const slug = createFilePath({ node, getNode, basePath: `pages` })
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    })
  }
}
exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions
  const result = await graphql(`
    query {
      allContentfulPost {
        edges {
          node {
            postTitle
            slug
          }
        }
      }
    }
  `)

  const posts = result.data.allContentfulPost.edges

  posts.forEach(({ node }, index) => {
    createPage({
      path: `${node.slug}`,
      component: path.resolve(`./src/templates/blogPost.js`),
      context: {
        slug: node.slug,
        prev: index === 0 ? null : posts[index - 1].node,
        next: index === posts.length - 1 ? null : posts[index + 1].node,
      },
    })
  })
}

Then I'm querying blog posts in 2 places: one as a blog page like this:

blog.js page:

function Blog() {
  return (
    <Layout>
      <SEO title="Blog Page" />
      <StaticQuery
        query={graphql`
          query blogQuery {
            allContentfulPost {
              edges {
                node {
                  slug
                  postTitle
                  postImage {
                    file {
                      url
                      fileName
                    }
                  }
                  postContent {
                    postContent
                  }
                  postDate
                }
              }
            }
          }
        `}
        render={data => (
          <ul>
            {data.allContentfulPost.edges.map(({ node }) => {
              return (
                <BlogPostsContainer>
                  <Link to={`${node.slug}`}>
                    <BlogPostImage src={node.postImage.file.url} />
                    <PostTitle>{node.postTitle}</PostTitle>
                  </Link>
                </BlogPostsContainer>
              )
            })}
          </ul>
        )}
      />
    </Layout>
  )
}

And the other, a react component which I use to display some of my posts in the homepage:

BlogSection.js

function BlogSection() {
  return (
    <BlogSectionWrapper>
      <StaticQuery
        query={graphql`
          query homeBlogQuery {
            allContentfulPost(limit: 2) {
              edges {
                node {
                  slug
                  postTitle
                  postImage {
                    file {
                      url
                      fileName
                    }
                  }
                  postContent {
                    postContent
                  }
                  postDate
                }
              }
            }
          }
        `}
        render={data => (
          <ul>
            {data.allContentfulPost.edges.map(({ node }) => {
              return (
                <HomePostsContainer>
                  <Link to={`${node.slug}`}>
                    <HomePostImage src={node.postImage.file.url} />
                    <PostTitle>{node.postTitle}</PostTitle>
                  </Link>
                </HomePostsContainer>
              )
            })}
          </ul>
        )}
      />
      <ButtonWrapper></ButtonWrapper>
      <FlatButton
        item={{
          title: "See all posts",
          icon: "/images/icons/book.svg",
          link: "/blog",
        }}
      />
    </BlogSectionWrapper>
  )
}

Now the problem is that when I click in a blog post from the home page (the ones rendered from the BlogSection.js component), the URL is normally http://localhost:8000/test-post.

But when I click on a post from the blog page (rendered from the blog.js file), the URL becomes http://localhost:8000/blog/test-post.

I also added Prev and Next buttons on each post and when I click on them, it just appends the slug in the URL, for instance when I'm on http://localhost:8000/test-post and click Next, it becomes http://localhost:8000/test-post/test-post2.

How can I keep the URL to show the post slug only or maybe with a /blog prefix for all the posts independent from where I click on it?

I tried this on the gatsby-node.js file but its not working:

posts.forEach(({ node }, index) => {
    createPage({
      path: `/blog/${node.slug}`, //<===== THIS LINE
      component: path.resolve(`./src/templates/blogPost.js`),
      context: {
        slug: node.slug,
        prev: index === 0 ? null : posts[index - 1].node,
        next: index === posts.length - 1 ? null : posts[index + 1].node,
      },
    })
  })
1 Answers

Your dynamic posts are being created at localhost:8000/:post-slug not under /blog/:post-slug because of:

  path: `${node.slug}`,

So, the first step is to add the following (what you've tried so far):

posts.forEach(({ node }, index) => {
    createPage({
      path: `/blog/${node.slug}`, 
      component: path.resolve(`./src/templates/blogPost.js`),
      context: {
        slug: node.slug,
        prev: index === 0 ? null : posts[index - 1].node,
        next: index === posts.length - 1 ? null : posts[index + 1].node,
      },
    })
  })

After that, keep in mind that all links that are not prefixed by /, will be concatenated to the current URL, like what happens with the standard anchor (<a> tag). Just adding a / at the beginning of the slug should do the trick. That explains this issue:

I also added Prev and Next buttons on each post and when I click on them, it just appends the slug in the URL, for instance when I'm on http://localhost:8000/test-post and click Next, it becomes http://localhost:8000/test-post/test-post2.

Now, if you want, for example, to go from the home page to a specific post, you will need to add the /blog to the URL since it's not a part of the post slug:

function BlogSection() {
  return (
    <BlogSectionWrapper>
      <StaticQuery
        query={graphql`
          query homeBlogQuery {
            allContentfulPost(limit: 2) {
              edges {
                node {
                  slug
                  postTitle
                  postImage {
                    file {
                      url
                      fileName
                    }
                  }
                  postContent {
                    postContent
                  }
                  postDate
                }
              }
            }
          }
        `}
        render={data => (
          <ul>
            {data.allContentfulPost.edges.map(({ node }) => {
              return (
                <HomePostsContainer>
                  <Link to={`/blog/${node.slug}`}>
                    <HomePostImage src={node.postImage.file.url} />
                    <PostTitle>{node.postTitle}</PostTitle>
                  </Link>
                </HomePostsContainer>
              )
            })}
          </ul>
        )}
      />
      <ButtonWrapper></ButtonWrapper>
      <FlatButton
        item={{
          title: "See all posts",
          icon: "/images/icons/book.svg",
          link: "/blog",
        }}
      />
    </BlogSectionWrapper>
  )
}

Notice the <Link to={`/blog/${node.slug}`}>. And do the same exactly with the blog.js template.

The previous behavior was working in blog.js because paths are not relative, it was concatenating the post slug to the current URL (/blog), but wasn't pointing to /blog.

To solve the further issues with that, check all the slug and add the / (if needed) to avoid concatenating issues.

Summarizing:

  • Create the dynamic posts under /blog/:post-slug in the gatsby-node.js: this will generate all posts under /blog
  • Point all links to /blog/:post-slug no matter the origin URL (home page to post, etc).
  • Add a slash (/) at the beginning of all links that point to the posts to avoid concatenating and relativity of URL issues. The same workaround applies to the next and previous buttons.
Related