We live in a world of complex Single-Page Application (SPA), ready-to-use CMSs and many tools that help us create static websites. Coding in classic form (pure HTML, CSS and JS) seems tedious and repetitive for a developer using the newest technologies. However, there is a solution for that. In the world of frameworks, we can find dozens (if not hundreds) of ready-made tools for creating static pages in a "programmer-friendly" way. The described problem appeared in our recently completed project. The client expected a static blog containing almost 70 static pages (4 different templates) and a 3d experience. Here Gatsby.js and Three.js appeared to help solve this problem. Having the right tools and knowledge, it would seem, that the rest of the project would be a piece of cake? It wasn't exactly like that. In this article I will try to present the biggest challenges - such as content structure and serving 3D graphics in a web browser.

Gatsby.js configuration and content structure

We started the configuration with research. We had a very agressive timeline for delivery a site without database or API references. That's why we wanted to do it the best we could, so that nothing needs to be improved - there is nothing worse than a wall of bugs a day before the release of the project.

Gatsby is a static site generator that truly has all the benefits expected in a modern web application. It does this by rendering dynamic React components into static HTML content via server side rendering at build time. Also Gatsby takes care of the basic configuration of the project. So we don't have to worry about setting up Webpack, React etc.

To build a content structure we have chosen JSON format. This gave us the ability to easily nest HTML tags, components and add attributes such as className. All within one file and format. This involved writing our own parser but at the same time gave us full control over the structure and its rendering.

{
  "tag": "div",
  "content": [
    {
      "tag": "p",
      "content": "Lorem ipsum",
      "properties": {
        "className": "paragraph-title"
      }
    },
    {
      "component": "Tiles",
      "content": [
        {
          "component": "TileItem",
          "content": {
            "header": "Title",
            "paragraph": "Lorem Ipsum"
          }
        },
        {
          "tag": "p",
          "content": [
            {
              tag: "span",
              content: "Lorem ipsum"
            },
            {
              component: "Link",
              content: "Test Link"
            }
          ],
        }
      ]
    }
  ]
}

A simple and clear structure made it possible for the customer to see the individual articles for themselves - we did not need any CMS for this solution. This saved us several days (choosing the right platform + setup) and at the same time did not discourage the customer. Win-win situation. However, the first hurdle appeared here. How to optimally generate pages from static files while having full control over them? The answer is Gatsby Lifecycle APIs. Gatsby.js lifecycle API

I created a structure - and then what?

To understand how to use this tool, we first need to understand how Gatsby.js works. Gatsby.js is a static web generator that collects data from the sources you provide and generates a web page for you. Easy, huh? Let's divide it into three separate layers:

  • Content layer - data sources
  • Building layer - graphQL processes received data
  • Presentation layer - generated static files (public package)

Let's start at the beginning. We needed an object containing the structure of a single page just before we started building our code. These words are almost the definition of onPreBootstrap - a function that Gatsby.js calls before it starts building pages. That's how we came to the solution, in which we serve all the content to the createPages plugin (creates pages with the URL passed from the JSON file) just before the build. Between these two actions we have a gap in which we can see what pages (and with what content) we have received. Wonderful, isn't it? Full control and almost unlimited possibilities when creating new subpages.

exports.onPreBootstrap = () => {
  fs.readdir('./content', (e, dirs) => {
    pageContent.forEach(page => {
      fs.readFile(`./content/${dir}/${pageFile}/${page}`, 'utf-8', (err, pageData) => {
        pages.push(JSON.parse(pageData));
      });
    });
  });
};
createPage({
  path: pagePath,
  component: path.resolve(`./src/templates/${pageTemplate}/${pageTemplate}.js`),
  context: {
    metadata: pageMetadata,
    staticData: pageStatic,
    dynamicData: pageContent,
  },
});

This is not an ideal solution, but in our case it worked perfectly. We had control without any loss of quality (in case of a generated package). Would we use this approach again? To be honest, currently (with more knowledge from the project) we would use more of the advantages of GraphQL. This requires a more precise configuration, separate plugins and a few additional internal functionalities (here mainly the React ones - like hooks). If it's so much trouble, is it worth it? Of course. With graphQL - in this particular case - we optimize the code almost twice.

JSon everywhere! - Let's use MDX

const useArticle = () => {
  const data = useStaticQuery(graphql`
    query {
      allMdx {
        nodes {
          frontmatter {
            author
            slug
            title
            image {
              sharp: childImageSharp {
                fluid(maxWidth: 350, maxHeight: 350) {
                  ...GatsbyImageSharpSizes
                }
              }
            excerpt
            }
          }
        }
      }
    }
  `)

Long story short - The hook downloads all MDX files (indicated in gatsby-congif.js configuration) and serves us particular nodes (a single node is a single page). In each of them there is a frontmatter containing data about the author, url, title, thumbnail and excerpt (the content - in the code below is the part beginning with the Link import). Just note, that frontmatter is not available in default configuration. We need to configure an additional plugin in gatsby-config.js - gatsby-transformer-remark.

---
title: Article Test - Gatsby Link
slug: article-test-01
author: Cognifide
image: ./images/gatsby-astronaut.png
---

import { Link } from "gatsby"

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam odio felis, tincidunt a odio ac, posuere fringilla orci. Pellentesque sapien nisl, interdum rutrum purus sit amet, pharetra convallis augue.

<Link to="/article-test-02">Article test 2 -></Link>

3D adventure

Now that we have finished the generated pages, it's time for the components! It looks much better here. Gatsby.js is based on React, the construction of components is no different from those in the SPA. The development started to progress very quickly. The number of pages was growing rapidly. Articles, subpages, landing page all based on JSON! Surprised by the speed of work we faced the last challenge. 3D experience. Listed articles "suspended in the air", with additional depth - even a journey into the depths of subsequent posts and many additional possibilities such as the operation of the gyroscope on mobile devices. Here the choice fell on Three.js

Three.js example image

Almost 850 lines of code - that's how long it took to create a full experience. Why so many lines of code? Let me explain the complexity of the problem. Creating a 3D view requires proper setup. Computer itself does not know the concept of the third dimension, the library helps us to achieve this effect by special mathematical operation and functions offered by the library (matrix, Vector3 etc). Do not be afraid of calculations and the structure of your 3D scene code. We have here (in the library) some analogy to the static pages and DOM tree i.e. THREE.Scene can be considered unambiguous to HTML tag , and THREE.Material use as { color: "blue" } in CSS etc.. This brings us to threeRenderer.render(scene) and treats it like "displaying" a ready website containing HTML and CSS. Hard to imagine? We flatten the structure of Three.js a little bit:

THREE.Scene('World')
|-THREE.Mesh('Article 1')
|-THREE.Mesh('Article 2')
...
|-THREE.Mesh('Article n')
|-THREE.Light('light')
|-THREE.Camera('main')

Not as black as it is painted... Although you need some fundamental knowledge about 3D here, don't be discouraged and try to use Three.js (even to see the code). There are examples available at https://threejs.org/ - this is the official website of the library showing what miracles we can do with its help.

Summary

Gatsby.js is very universal which I have experienced many times. The basis is the knowledge of React, which may be for some people a disadvantage. But are you sure? Both frameworks (Gatsby.js and React) has very good documentation and a community that allows everyone to create a simple blog or product page. I don't want to favour a particular technology stack (although the development process was very pleasant). However, our work was not entirely a bed of roses. This article is meant to warn against avoiding graphQL, and on the other hand, to show the freedom of serving content (Using e.g. the JSON format). Don't you want to use graphQL? You can write a parser of any structure yourself. Unfortunately, this way is not always the right one, as we've seen. If today I was faced with the decision to choose a tool to build a static website without hesitation, I would choose Gatsby.js.