New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] Can I build an e-commerce site with Gatsby? #2586

Closed
ooloth opened this Issue Oct 22, 2017 · 25 comments

Comments

Projects
None yet
8 participants
@ooloth
Contributor

ooloth commented Oct 22, 2017

I'm confident that a static site (like the one generated by Gatsby) can handle the static aspects of an e-commerce site quite well (listing product details, etc.). But, can it also handle the dynamic aspects (like a shopping cart and checkout process)?

I'm hoping to use Gatsby for as many sites as possible in the future and trying to understand if e-commerce will be a good fit...

Moltin or WooCommerce (via the WordPress REST API) both look like they might be good ways to add this functionality, but since my experience in this area is limited, I'd love to hear the advice of any Gatsby experts out there.

Should Gatsby be used for e-commerce? Are Moltin or WooCommerce feasible ways to do that? Are there better ways?

Any guidance would be very appreciated!

@KyleAMathews

This comment has been minimized.

Contributor

KyleAMathews commented Oct 22, 2017

As long as the shopping cart system has a good API for handling the checkout then you should be fine! Because Gatsby doesn't have a default backend, you need to handle shopping carts & checkout through ajax calls. But because Gatsby is built with React, handling ajax calls and responses is easy.

@matthew1809

This comment has been minimized.

matthew1809 commented Oct 23, 2017

Moltin works well with React, there's an example store using react and redux, you should be able to hook Gatsby up pretty easily. Let me know if you'd like to chat about it further!

@ooloth

This comment has been minimized.

Contributor

ooloth commented Oct 23, 2017

@KyleAMathews That's great news! Much appreciated. Gatsby makes me happy. :)

@matthew1809 Awesome! That example store will be super helpful. I'll definitely get in touch when I'm implementing a store - thanks for that offer!

@ooloth ooloth closed this Oct 23, 2017

@hhsadiq

This comment has been minimized.

hhsadiq commented Feb 4, 2018

Hey @ooloth just checking in to know your experience of building an e-commerce website with Gatsby. I am also planning to build an e-commerce site. The only hurdle which I am trying to overcome is that we have RESTFul APIs in production which are serving the live mobile applications, so basically they are battle-tested ready to consume APIs. I would like to use these APIs but looks like Gatsby enforces to create a middle GraphQL based server to consume the RESTFul APIs. That will become a tedious work. Is there a way to use the RESTFul APIs without the pain of adding middle-data-layer of GraphQL?

Looks like it was the same issue which was mentioned by the creator of react-static, in his medium article.

Using external data in Gatsby requires that you use an intermediary GraphQL database to query that content. This felt extremely superfluous given that our data was almost always coming from a CMS, and we didn’t really see any extra value added to our ability to supply data to pages that we couldn’t already do with javascript. Attempting to bypass this was a nightmare and honestly made us want to go back to Next.

I know that is an old post, and maybe Gatsby offers some solution now.

Maybe @KyleAMathews can also help.

@ooloth

This comment has been minimized.

Contributor

ooloth commented Feb 4, 2018

I haven't needed to build an e-commerce site with Gatsby quite yet (I was asking in advance), but I'll be very interested to hear how things go for you!

@sandys

This comment has been minimized.

sandys commented Feb 23, 2018

hi @ooloth @hhsadiq @KyleAMathews im also interested in this. We have a gatsbyjs site and we want to add a fair bit of dynamic pages ... not dissimilar to an ecommerce site.
For example a list of products on the home page (which comes from an api)

/product/1234 -> should render the product page based on data from an external api.

so its kind of a mix between a static site and a dynamic site. we have been hesitating to jump ship to gatsbyjs because it seems that this is a bit hard to do. any experience from you guys.
that paragraph from the medium article makes it a bit concerning :(

@pieh

This comment has been minimized.

Contributor

pieh commented Feb 23, 2018

@sandys You can do dynamic parts of pages (normal react way) or whole pages (on client) - https://www.gatsbyjs.org/docs/building-apps-with-gatsby/

@hhsadiq

This comment has been minimized.

hhsadiq commented Feb 23, 2018

@pieh thanks.

So basically we can use plain old react to fetch the dynamic data using normal AJAX/API calls. That is awesome. Just one question left. When fetching the data for static (most powerful) part of gatsby, we have to use GraphQL..right?

Actually, the only hurdle in fully adopting the gatsby in our current production project is GraphQL. Although we really like the GraphQL and have decided to use it for new projects, but since the current systems are already built using REST architecture, so re-writing REST into GraphQL servers is a real pain and nobody in the company wants to do that.

So if we can somehow bypass the GraphQL, we will be good to go.

@pieh

This comment has been minimized.

Contributor

pieh commented Feb 23, 2018

@hhsadiq you can pull data from your rest API and insert it into gatsby data store (this is actually what most of the source plugins do). Gatsby will automatically create graphql schema and queries you can use based on data you put into its store.

Reason it was designed like that is because in graphql queries you can specify exact minimal subset of data that is needed to render the page so when page is loaded your website users will download only data they need to render it. It also remove the need of chained request (for example: download post data, then get detailed info about author of the post and his other posts for "other posts by this author" - this is all done in single request with only data that is needed - you don't show author hobbies on post page - no worries it doesn't have to be downloaded, but you can display them in dedicated page about the author)

@sandys

This comment has been minimized.

sandys commented Feb 23, 2018

@pieh what you said above is pretty much superb. Both for dynamically created pages (/products/1234) and getting data from rest api and making it available as graphql data (as part of the app and not as separate plugin) - are there any sampls/starters for this ?

this is what we have been struggling with

@pieh

This comment has been minimized.

Contributor

pieh commented Feb 23, 2018

Information on this isn't really consolidated in one self contained guide or tutorial (maybe we should put more focus on it).

I'll try to write up short intro here so it would be easier to dig up more detailed information in documentation:

Gatsby for its data store use collection of Nodes ( https://www.gatsbyjs.org/docs/node-interface/ ). Each node represent single data entity (for example single blog post, single author/user, single product, single category, single github repository, single post on facebook, single project on behance - it's up to you to define what it represent). Every node has type to identify what it is (markdown files will be of type MarkdownRemark, files will be File and products from your store/api you can define as of type MyProduct or however you like). Gatsby will automatically group nodes by its types and create graphql entries so you can query them (using name of type as base - allMyProduct for connection type queries where you can sort, filter, limit etc to get array of results and myProduct to get singular result).

There is single gatsby API function called createNode ( https://www.gatsbyjs.org/docs/bound-action-creators/#createNode ) used to populate data store (there are more related functions but for basic example this is enough). In example in docs I linked for that function you can see that there are some required fields that gatsby need to properly process node, but you can add any other fields you like (or whatever data comes from your API) - those fields don't have to be scalars - it's completely fine to use arrays or objects there too.

There is just one more requirment to use that function - you can't do that anywhere - this function has to be called during gatsby bootstraping stage - more specifically in sourceNodes step ( https://www.gatsbyjs.org/docs/node-apis/#sourceNodes - you can use example from documentation and paste that in your project's gatsby-node.js). If you have async operation (like getting data from API), then return sourceNodes function need to return promise, so gatsby know it has to wait for it to resolve before continuing with bootstrap process (after that step gatsby will create graphql schema automatically).

Usually sourcing data is abstracted to separate plugins (source plugins) but you don't have to do that - plugins use exactly same gatsby apis that you can use in your project - it's just it's nicer to abstract data sourcing to separate package and additional benefit is that they can be easily reused between projects.

Simplest example I could find is gatsby-source-faker - it just use faker package to generate dummy data:
https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-faker/src/gatsby-node.js - it's pretty barebones and I hope with introduction above it's quite understandable (especially if You used faker before)

Also some links relevant to this topic:

@sandys

This comment has been minimized.

sandys commented Feb 23, 2018

@pieh this is the most awesome explanation that i have found anywhere! thanks so much for that.

you can pull data from your rest API and insert it into gatsby data store (this is actually what most of the source plugins do).
is there an example of this - pulling data from rest API and inserting into the data store ?

I'm trying to take what you wrote above and am trying to see how it melds with dynamic pages.
Let's say we have a dynamic page /product/1234 . it needs some data from a REST api to render this page.
Will this go through the createNode flow ? Intuitively it feels not ...

@hhsadiq

This comment has been minimized.

hhsadiq commented Feb 26, 2018

@pieh Thanks a lot for the detailed answer. I guess it pretty much answers the question. So we don't need to re-write our current RESTFul APIs to GraphQL. We can just fetch the data from them and gatsby will store it in data-store. And the application can query that data using GraphQL.

And of course, we can always use the AJAX calls to pull the dynamic data. With this, we are good to use gatsby in e-commerce site. Cheers 🥇 👍 🥇

@sandys

This comment has been minimized.

sandys commented Feb 26, 2018

An example to do this (fetch from RESTFUL api and store it in datastore) will be very helpful !

@pieh

This comment has been minimized.

Contributor

pieh commented Feb 26, 2018

@sandys I've missed your comment before weekend :(
Dynamic pages (as in - pages not creating during gatsby build) would have to fetch data on client side same as in any react app and there's nothing gatsby specific to that - You run your ajax request in your component to get data.

But if this data doesn't change every few seconds/minutes it's generally better to rebuild page with gatsby (as this brings all the benefits gatsby offers). And in this case createNode will be needed.

I will write up simple example using https://jsonplaceholder.typicode.com/ as source of data today when I'm done with work. I'm thinking to use /todos (it seems like go-to for tutorials/samples :D) and /users (to show how to link nodes - each todo has user associated with it)

@pieh

This comment has been minimized.

Contributor

pieh commented Feb 26, 2018

@sandys
Here's basic example and I hope with comments will be understandable. I've used axios to fetch data from REST, but gatsby doesn't really care library you would use - anything that works in nodejs will work fine.

gatsby-node.js:

const axios = require(`axios`)
const crypto = require(`crypto`)

// Hook into sourceNodes step to create nodes
exports.sourceNodes = ({
  // Extract createNode function from boundActionsCreators
  boundActionCreators: { createNode },
  // Helper function to create unique id in gatsby data store
  createNodeId,
}) => {
  return new Promise((resolve, reject) => {
    axios
      .get(`https://jsonplaceholder.typicode.com/users`)
      .then(response => {
        const users = response.data

        users.forEach(user => {
          const userNode = {
            // All information we got from REST endpoint
            ...user,

            // We want to preserve original id, it would be overwritten by
            // gatsby specific node id (more about node id below) 
            original_id: user.id,

            // Gatsby specific fields:

            // Gatsby need globally unique node id - for this we use helper
            // function createNodeId that will ensure that created id
            // is unique
            id: createNodeId(`user_${user.id}`),

            // parent and children are used in transformer type plugins
            parent: null,
            children: [],

            internal: {
              // Gatsby will create entry points:
              // - `allUser` to get collection of nodes with methods
              //   to sort, filter, group, use aggregate functions etc
              // - `user` to get single node with option to filter/select
              type: `User`,
              // contentDigest is something like hashCode in Java - this is
              // internally used to compare node we create and cached node
              // (gatsby does cache node data between builds) to determine
              // if node data has changed
              contentDigest: generateContentDigest(user),
            },
          }

          // finally create node
          createNode(userNode)
        })

        // We fetched and processed all data, let gatsby know we're done
        resolve()
      })
      .catch(reject)
  })
}

/**
 * Helper function to generate contentDigest from data of the node
 */
const generateContentDigest = data =>
  crypto
    .createHash(`md5`)
    .update(JSON.stringify(data))
    .digest(`hex`)

And this is all that's needed to be able to query like this:
9d3d5ee1b80d763609bda92dab20af00

I will add next example that will add todos and link between todos and users later.

@sandys

This comment has been minimized.

sandys commented Feb 26, 2018

this is super brilliant!
thank you so much for this - yes having this in the examples would help so much!

@pieh

This comment has been minimized.

Contributor

pieh commented Feb 26, 2018

So this is example with todos (linked nodes) - it use same base with some refactoring (abstracted node object creation to helper function as now we would need to do this for both users and todos)

const axios = require(`axios`)
const crypto = require(`crypto`)

// Hook into sourceNodes step to create nodes
exports.sourceNodes = ({
  // Extract createNode function from boundActionsCreators
  boundActionCreators: { createNode },
  // Helper function to create unique id in gatsby data store
  createNodeId,
}) => {
  return Promise.all([
    axios
      .get(`https://jsonplaceholder.typicode.com/users`)
      .then(response => response.data),
    axios
      .get(`https://jsonplaceholder.typicode.com/todos`)
      .then(response => response.data),
  ]).then(([users, todos]) => {
    // To link nodes gatsby use following convention:
    // fields that are name like this nameOfField___NODE will be converted to
    // links (___NODE part will be removed - this is just to let gatsby know
    // that this field contain id or array of ids of nodes we want to link).

    // Construct nodes for todos
    const todoNodes = todos.map(data =>
      constructNodeFromRestData({ data, type: `Todo`, createNodeId })
    )
    // Construct nodes for users
    const userNodes = users.map(data =>
      constructNodeFromRestData({
        data: {
          ...data,
          // here we init array, that will later be populated with ids of
          // todo nodes. Note that in rest response users don't have
          // list of theirs todos - we will add that so we can navigate
          // from todo to user and from user to todos in queries 
          todos___NODE: [],
        },
        type: `User`,
        createNodeId,
      })
    )

    todoNodes.forEach(todoNode => {
      // get user node of author os this todo
      const userNode = userNodes.find(
        userNode => userNode.original_id === todoNode.userId
      )

      if (userNode) {
        // add user to this todo node
        todoNode.user___NODE = userNode.id

        // add this todo to list of user todos
        userNode.todos___NODE.push(todoNode.id)
      }

      // Optionally we can remove userId field as this will not bring any value
      // and will just polute schema. Deletion is not needed if name of the link
      // we create is different than field we use to construct that link.
      // If that field would be named user (and not userId) then this would
      // be necessary to avoid ambiguity.
      delete todoNode.userId
    })

    // finally create nodes
    todoNodes.forEach(node => createNode(node))
    userNodes.forEach(node => createNode(node))
  })
}

/**
 * Helper function to generate contentDigest from data of the node
 */
const generateContentDigest = data =>
  crypto
    .createHash(`md5`)
    .update(JSON.stringify(data))
    .digest(`hex`)
/**
 * Helper function that generate node object from rest data
 */
const constructNodeFromRestData = ({ data, type, createNodeId }) => {
  return {
    // All information we got from REST endpoint
    ...data,

    // We want to preserve original id, it would be overwritten by
    // gatsby specific node id (more about node id below)
    original_id: data.id,

    // Gatsby specific fields:

    // Gatsby need globally unique node id - for this we use helper
    // function createNodeId that will ensure that created id
    // is unique
    id: createNodeId(`${type}_${data.id}`),

    // parent and children are used in transformer type plugins
    parent: null,
    children: [],

    internal: {
      // Gatsby will create entry points:
      // - `allUser` to get collection of nodes with methods
      //   to sort, filter, group, use aggregate functions etc
      // - `user` to get single node with option to filter/select
      type, // (from argument)
      // contentDigest is something like hashCode in Java - this is
      // internally used to compare node we create and cached node
      // (gatsby does cache node data between builds) to determine
      // if node data has changed
      contentDigest: generateContentDigest(data),
    },
  }
}

Result:
da103c2bdec3bbc700cbad54212c74a4

@hhsadiq

This comment has been minimized.

hhsadiq commented Feb 28, 2018

Great example @pieh. Thanks for taking the time to write it. 👍

@pieh pieh referenced this issue Feb 28, 2018

Closed

[docs] Improve documentation for plugin authors #4266

2 of 8 tasks complete
@vnwarrior

This comment has been minimized.

vnwarrior commented Apr 2, 2018

@pieh this is the only place we could find any information about dynamic client side sites in gatsbyjs. Two questions:

  1. https://www.gatsbyjs.org/docs/building-apps-with-gatsby/ talks about using "matchpath" to create client only routes. You havent mentioned it here. Is it at all necessary ? I'm getting confused on what matchpath is supposed to do
  2. Can we use react-router ? basically we have a complex client side react application with multiple screens. Can we integrate that in gatsbyjs - i have tried a HUGE number of combinations, but nothing seems to work. How do we build react routing into gatsbyjs for client side applications with multiple components.
@pieh

This comment has been minimized.

Contributor

pieh commented Apr 2, 2018

@vnwarrior
Gatsby uses react-router under the hood. matchPath argument in createPage is used to allow dynamic paths with params - you can't use Router components because gatsby uses it already, but you can use Route components inside your pages

You can check https://github.com/gatsbyjs/gatsby/blob/master/examples/client-only-paths to see how to implement dynamic paths:
matchPaths used in gatsby-node.js - https://github.com/gatsbyjs/gatsby/blob/master/examples/client-only-paths/gatsby-node.js to specify that index route will handle all the paths (depending on your use - you might limit this to specific pages - for example to handle search requests - /search/:query in your /search page) and then page component that handle dynamic params: https://github.com/gatsbyjs/gatsby/blob/master/examples/client-only-paths/src/pages/index.js

@vnwarrior

This comment has been minimized.

vnwarrior commented Apr 3, 2018

@pieh - thanks for answering. Still some confusion. So I'm asking this in context of an ecommerce site (which is what this bug is about).

I will have /products -> dynamic list of products from api
/product/:id -> details about a single product. also coming dynamically from api.
"add to cart" button on /product/:id will lead to /cart page which will have cart . This cart should ideally come from redux.

so the question I have is this (for which there is no example) - how do I add an item to redux from /product/:id and then redirect/render to the Cart component (while changing the url to /cart). And on /cart page, I now will fetch data from Redux.

This is fairly straightforward to do in vanilla react. However, it is not very intuitive on how to do on Gatsby - the wiring of redux + redirection (from product to cart page) while taking care of urls is not intuitive. If you could modify the client-only-paths to use redux + redirect from one component to another, then it would be a big help to building ecommerce type sites.

@pieh

This comment has been minimized.

Contributor

pieh commented Apr 3, 2018

I think that for products pages you'd rather want to use static pages (insert data from your api to gatsby data store and create pages for each product at build time) and not dynamic pages (which lose a lot of benefits of using gatsby).

But this doesn't really change your other questions:

Re redux: only thing in gatsby that need special handling is setting up redux provider - which you can see how to to in https://github.com/gatsbyjs/gatsby/tree/master/examples/using-redux (gatsby-ssr and gatsby-browser files). Rest of using redux is same as with any react app - what problems do You have with this? Maybe I just don't see exactly where the problem is with this.

Re redirects:

  • if you want programatic redirects you can use navigateTo function exported from gatsby-link (which is wrapper around history.push function with some gatsby specific stuff to prefetch resources needed for page you will navigate to before changing location)
  • or if you want declarative redirects you can use <Redirect> from react-router
@vnwarrior

This comment has been minimized.

vnwarrior commented Apr 7, 2018

@pieh - over last few days, I have been trying this out. I found a repo https://github.com/austintgriffith/cryptogs/blob/dca01f882904b1ed8eb2ec5302aa57a2df116bbc/gatsby-site/gatsby-node.js that does this .
I had two questions and I hope you wouldnt mind answering it:

there is no matchpath for /stacks -

  1. https://github.com/austintgriffith/cryptogs/blob/dca01f882904b1ed8eb2ec5302aa57a2df116bbc/gatsby-site/src/pages/stacks.js . yet it seems to work fine as client side. Is matchpath at all needed ? Wondering what situations is it really needed and whether client side pages be written without it ?

  2. Should we install react-router explicitly or use the one packaged inside gatsby ? I was making an error here and didnt know if we should explicitly install this again (like here)

@pieh

This comment has been minimized.

Contributor

pieh commented Apr 7, 2018

I'm not familiar with this page, but /stacks page doesn't seem to have or handle any path params - matchpath is only needed if you want to handle dynamic params like /path/:param - if you don't need param then matchpath is not needed.

I'm pretty sure you shouldn't install react-router separately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment