How I Built My Website Using Gatsby, Strapi, and AWS

I spent a lot of time researching the best combination of complementary frameworks to build my website. I eventually settled on using Gatsby to develop the website and Strapi for the CMS. In this post, I'll discuss why I chose these frameworks, and how I built the website.

Matthew Stubbs

19th Mar 2021

I have been meaning to build my website for some time, but until recently have been too busy with other projects to give it the attention it deserves. But two months ago, I had a break between projects and set myself the task of finally building this website.

First, let's discuss a few requirements for the website.

I wanted to use a modern development framework based on either Angular or React. I have used angular for most of my web-based projects, so I wanted to use this project to improve my competency with React. The website was likely to be updated once or twice a week with new content, and so needed to be updated with relative ease. For this, I needed to use some form of CMS. The website, containing project overview and blog articles, was going to be heavy on image data. Whichever framework I chose was going to have to handle this to minimise website loading time, to avoid heavily impacting the performance of the site, which would, in turn, affect the SEO.

The first decision to make was should the website be static or dynamic?

Static Vs. Dynamic

Websites are separated into two different types: static and dynamic. Static websites are ones that are fixed and display the same content for every user. A dynamic website, on the other hand, is one that can display different content and provide user interaction, by making use of advanced programming and databases in addition to HTML.

Dynamic sites allow for a high degree of functionality, but this is not without a cost. Dynamic websites are far more difficult to optimise for SEO, and often come with a significant load time, especially if using client-side rendering.

Static websites on the other hand are often faster and perform better for SEO. However, the content is, perhaps not surprisingly, static.

Gatsby to the rescue

Gatsby JS is a React-based, GraphQL powered static site generator. Got it? Well, if not, don't worry, here's what you need to understand.

Gatsby allows you to build a site using a dynamic framework, that framework being React. This offers you many of the advantages of this form of development. However, the magic of Gatsby comes from how it builds your website. When you build and deploy your website, it uses powerful preconfiguration to build a website that uses only static files for incredibly fast page loads due to its utilisation of service workers, code splitting, server-side rendering, intelligent image loading, asset optimization, and data prefetching.

How Gatsby Works

This fit my requirements perfectly. I am used to developing dynamic websites and prefer this form of web development. But I did not want the associated overhead of running such a site, and SEO was an important consideration from the outset.

Gatsby allowed me to build using React, a modern dynamic web framework, but when built transformed this into a directory with a single HTML file and my static assets. This resulted in an incredibly fast, beautifully simple, SEO optimised website. Magic!

A fully customisable CMS

There are loads of Content Management Systems (CMS) to choose from these days, the most popular one, of course, being Word Press. I find it easiest to think about CMS's on a spectrum.

On one end you get CMS's that require minimal work to get running and can be as simple as signing up for an account and pulling your content from the plug and play API. These require little to no maintenance, with the drawback being that they often have limited customisation, and can be expensive. A good example of this is Contentful, a cloud-based CMS that is simple to set up and commonly used in a stack with Gatsby.

At the other end, you have your developer focussed CMS's. These are usually fully customisable, but are often more difficult to use, and require that you host the CMS yourself. Strapi, a fully customisable, open-source CMS, is a good example of this.

I did a lot of research into the best CMS to use, and both types of CMS have strong pro's, as well as cons. However, as I was using this project to push my skill set, I settled on Strapi. This had the added benefit that it is completely free to use, the only cost incurred is that of hosting the CMS myself.

Strapi Content Builder Image

A few great things about Strapi:

  • Open Source: The entire codebase is available on GitHub and maintained by hundreds of contributors. I've always had a soft spot for open-source technology.
  • Customisable: Very easy to customise API and admin panel. There are also lots of custom plugins to extend to platforms functionality.
  • GraphQL API option: As Gatsby uses GraphQL for its network interactions, this was non-negotiable. GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. It is an alternative to the more commonly used RESTful API.

The design

As SEO optimisation was a primary goal for the website, mobile optimisation was essential.

As of September 2020, Google made the switch to mobile-first indexing on all websites, as opposed to desktop crawling that the engine has long preferred.

This means that if a website does not format well on mobile devices, then it will not index highly on Google.

mobile-first.png

Fortunately, building responsive websites that look good on both mobile and desktop devices is easy to do using a toolkit such as Bootstrap.

Bootstrap allows you to quickly design and customize responsive, mobile-first sites. The toolkit is open-source, featuring Sass variables and mixins, a responsive grid system, extensive prebuilt components, and powerful JavaScript plugins. There is also a node package React-Bootstrap that makes working with this package in React much easier.

Home page hero design

I have had a lot of positive comments about the hero banner on the website home page. However, I cannot claim credit for the original idea of this design, which has the go to Adham Dannaway - I love the split design of his hero image, and I felt this design worked perfectly for The Dev Doctor website, showcasing both the developer and doctor side of my work.

I think it's a great design Adham came up with, and after viewing a lot of developer sites featuring a similar hero image design, it seems I'm not the first to think so.

Portfolio timeline

The next important design component of the website is the portfolio timeline, and this I can claim credit for. I wanted to create an animated timeline to showcase my work in reverse chronological order. Timelines are often displayed in a horizontal format, but horizontal scrolling, outside of something like a carousel, is unnatural on both desktop and mobile. So I settled on creating a vertical timeline.

The next issue I faced was how to make this timeline work for both desktop and mobile. The desktop design was easy, having a central timeline with projects that could animate in either side of the timeline to maximise the use of the available space.

However, this design would not work on a mobile device due to there not being an adequate width to have a central timeline. The best solution to this would be to have a timeline along one side, with projects animating on only one side of the timeline.

The dev doctor website timeline design

But what was the best way to do this without having to create 2 separate versions of the timeline for each screen size. Fortunately, this design can be created using Bootstraps grid, which under the hood uses the flex-box property in css. By reordering the columns depending on screen size, I was able to change the position of the timeline and portfolio items depending on whether the timeline was displayed on desktop or mobile.

// Frameworks
import React from 'react'
import { Col, Row } from 'react-bootstrap'
import { IoChevronDown } from 'react-icons/io5'
import { graphql, Link, useStaticQuery } from 'gatsby'
import Img from 'gatsby-image';

import styles from './vertical-timeline.module.scss'

const VerticalTimeline = () => {

    const projects = useStaticQuery(graphql`
        query {
            allStrapiProject(sort: {fields: publishedAt, order: DESC}) {
                edges {
                    node {
                            strapiId
                            slug
                            description
                            title
                            tagline
                            backgroundColor
                            publishedAt(formatString: "MMM YYYY")
                            previewImage {
                                childImageSharp {
                                        fluid(maxWidth: 500, quality: 100) {
                                            ...GatsbyImageSharpFluid_withWebp
                                        }
                                    }
                                }
                            
                            appLogo {
                                childImageSharp {
                                        fluid(maxWidth: 50, quality: 100) {
                                            ...GatsbyImageSharpFluid_withWebp
                                        }
                                    }
                                }
                            
                        }
                    }
                }
            }
    `)

    return (
        <div className={styles.timeline}>

            {projects.allStrapiProject.edges.map((edge, index) => {
                const project = edge.node

                return (<Row noGutters={true} key={index}>
                    <Col xs={{ span: 10, order: 2 }} md={{ span: 5, order: index % 2 > 0 ? 'last' : 'first' }}>
                        <div className={`${styles.smallScreenContainer} d-xs-flex d-sm-flex d-md-none d-lg-none d-xl-none`} data-sal="zoom-in">
                            <div className={styles.date}>{project.publishedAt}</div>
                            <div className={`${styles.imageContainer}`}>
                                <Img fluid={project.previewImage.childImageSharp.fluid} className={styles.previewImg}></Img>
                            </div>
                        </div>

                        <div className={styles.cardContainer}>
                            <div className={`${styles.projectCard} card`} style={{ borderColor: project.backgroundColor }} data-sal={index % 2 > 0 ? 'slide-left' : 'slide-right'} data-sal-duration="1000" data-sal-delay="250">
                                <div className={styles.header}>
                                    <h3>{project.title}</h3>
                                    <h6>{project.tagline}</h6>
                                    <div className={`${styles.accent} title-accent`}></div>
                                </div>
                                <div className={styles.featuredDescription}>
                                    <p>
                                        {project.description}
                                    </p>
                                </div>

                                <div className={styles.learnMore}>
                                    <Link to={'/project/' + project.slug}>
                                        <p>Learn more</p>
                                        <IoChevronDown></IoChevronDown>
                                    </Link>
                                </div>
                            </div>
                        </div>
                    </Col>
                    <Col xs={2}>
                        <div className={styles.lineContainer}>
                            <div className={styles.line}></div>
                            <div className={styles.circle} style={{ background: project.backgroundColor }} data-sal="zoom-in" data-sal-duration="1000">
                                <Img fluid={project.appLogo.childImageSharp.fluid} className={styles.logoImage}></Img>

                                <div className={`${styles.date} ${index % 2 > 0 ? styles.reverse : null}`}>{project.publishedAt}</div>
                            </div>
                        </div>
                    </Col>
                    <Col xs={0} md={{ span: 5, order: index % 2 > 0 ? 'first' : 'last' }} className="d-none d-md-flex">
                        <div className={styles.imageContainer} data-sal={index % 2 > 0 ? 'slide-right' : 'slide-left'} data-sal-duration="1000" data-sal-delay="500">
                            <Img fluid={project.previewImage.childImageSharp.fluid} className={styles.previewImg}></Img>
                        </div>
                    </Col>
                </Row>)
            })}
        </div>
    )
}

export default VerticalTimeline

Article layout

For the blog and project pages, I wanted to recreate the look and feel of Medium, a popular blogging website. Medium has an excellent design for online articles. By restricting the options you have to format your articles, at least in comparison to most WYSIWYG editors, they've been able to create a much cleaner, more aesthetically pleasing article page - at least that's my opinion.

I particularly like the options they have for image width. While there are the standard settings, being able to set the image to the width of the article, or smaller. They also have the option to set the image to be slightly wider or the full width of the screen. This makes for a really nice effect, particularly if you have a beautiful image you want to accentuate.

ScreenWidth.pngWideWidth.pngArticleWidth.png

This raised the question of how best to do this. I experimented with using negative margins, making use of the calc() CSS function to calculate the necessary negative margin. This worked, but the user experience was jarring, especially when changing screen widths.

The solution I settled on was to wrap the article in a container set to display as a flex-box container, which was set to the full-screen width. The article could then be centred within this container and a max-width set to the chosen article width. For images I wanted to be wider than the article width, it was as simple as changing the max-width for that image. This provided a much better solution than calculating negative margins.

The next issue was one I had not foreseen but only became apparent when connecting the website to the CMS.

Each article content is stored as markdown. Markdown is a lightweight markup language for creating formatted text using a plain-text editor, and it is the industry standard for storing this sort of text information. A drawback of markdown is that you're not able to store information about how to format the content it contains. For example, the standard method to change the image size would be to change its CSS class. However, this is not possible in content stored in markdown.

This had me genuinely stumped for a while. Markdown allows you to write HTML directly as part of the text data, and this would allow me to write classes as part of that. But writing HTML code in markdown text every time I wanted to display an image was a poor, hacky solution, and one I wanted to avoid if at all possible.

Fortunately, I could.

With some further research, I was able to find a way of specifying the image format directly in the markdown text, without having to write any HTML into the markdown field.

In CSS, you commonly target elements using classes or IDs, but these are not the only attributes you can use to target elements in the HTML code. You can actually use any of the element's attributes as selectors. Therefore, I could use the source attribute on the image as a CSS selector.

But this alone wasn't helpful, as the source would always be different, which is obviously useless as a selector. However, we can make this source useful by adding a URI fragment. URI fragments are used for a number of reasons, one example is as an anchor to a particular place on a page. Importantly, when the browser uses the URI to get an image from a database, it ignores the URI fragment, so adding one won't break our links for our images. But we can target this fragment in the source URI as a CSS selector, and therefore apply custom styling to the element.

So if I have the following URI to an image:

https://www.mydatabase/some-image

Simply by adding a URI fragment:

https://www.mydatabase/some-image#screen-width

I can target the element and add the styling. I can further optimise this by using the CSS operator $=. This operator will look for a match at the end of the URI. As I know the fragment will always be at the end of the URI, this is the most efficient way to compare the strings, therefore reducing the workload on the browser.

img {
		&[src$="article-width"] {
				width: 100%;
				max-width: 800px;
		}
	
		&[src$="wide-width"] {
				min-width: 800px;
				max-width: 1200px;
				width: 90%;
		}
	
		&[src$="screen-width"] {
				width: 100vw;
				max-width: 2200px;
		}
}

Front-end hosting

You can host Gatsby websites in the same way you host any other website, this is because Gatbsy produces static HTML files during its build process. So once the site has been built, to the browser it looks no different than any other static website.

I decided to host this website using AWS amplify, mainly because I hadn't used this service before, and I have to say, I was not disappointed.

The service connects directly to a remote git repository, I used GitHub, but you can use whichever service you prefer. Builds are automatically triggered whenever I push to the master branch, and linking to a custom domain bought through route 53 was seamless. AWS also offer a free SSL certificate for your domain. The whole process took less than half an hour and after making some minor corrections to the redirect policy I had a Gatsby site running behind my custom domain name using a secure protocol.

I must say, I have not used a system that slick before, so it's a thumbs up for AWS Amplify.

AWS amplify

You can also set up test domains to build off alternative branches in your repo, allowing you to create a full test environment before pushing to your master branch.

Strapi integration

It was easy to set up a Strapi development server, and I actually used their Gatsby blog template to speed things up further. There is also an official plugin for integrating Strapi into Gatsby via GraphQL.

When it came to hosting the production Strapi instance on a remote server, I found by far the simplest set up was using Heroku. A few button clicks, and my remote Strapi server was up and running.

I could also have hosted Strapi on AWS amplify, where the website is currently hosted, and long term I think I will migrate the backend to AWS amplify so that both are in the same place. But given my timeline for completing the website, the simplicity of Heroku for hosting Strapi swayed me over the simplicity of having both on the same platform.

There are a couple of important things to note when deploying Strapi server for production use.

Firstly, the database will not be copied, meaning all of the content you have created in the development server will not be present for the production instance. This was something I was not prepared for, and recreating all of the content was a little bit time-consuming. I'm telling you this so that you don't spend hours making your content on the development server perfect, cause that would be a really stupid thing to do (cough, cough).

Secondly, when the server is running locally for development, the images you upload are stored locally, effectively using your device as the database. However, when you deploy to the remote server for production, you need to supply a database to store the files you upload. Otherwise, any media you upload disappears after a few minutes, as the data is only stored in the server's cache. I used an AWS S3 database to host the images, and there is a really simple plugin to integrate Strapi with the database which can be found here.

The magic of Gatsby Image

As already mentioned, my website was always going to be fairly heavy for image data. Therefore, I needed an efficient way of optimising images to ensure this didn't negatively impact the pages speed, which would in turn impact the sites SEO.

The efficiency of an image is affected by a number of things:

File Type - the type of file (e.g. PNG, JPEG, WebP) affects its size. Very often, much of the data associated with these files is not required for use on the internet. Optimising for the appropriate file type can rapidly increase load time.

Image Dimensions - The larger the image, the larger the amount of data needed to transmit over the internet. By making sure images are of an appropriate size, i.e. the maximum size that they will be needed, but no larger, we can optimise the amount of data we need to transmit.

Method of loading - If you enforce all images on a page to be fetched when loaded, then this can block the page from displaying in a timely manner, as the page will wait till the browser has loaded all these elements. This will negatively impact site usability and loading time. Google also doesn't consider a page completely loaded until all elements above the fold are shown. And slower load times mean poorer SEO.

To do all of this manually requires a lot of work. It would take hours to produce all the images at the appropriate dimensions for each screen size, as well as to produce lower quality images to load as a placeholder before their higher-quality counterparts.

However, using Gatsby Image, this is handled for us.

Part of what makes Gatsby sites so fast is its recommended approach to handling images. gatsby-image is a React component designed to work seamlessly with Gatsby’s native image processing capabilities powered by GraphQL and gatsby-plugin-sharp to easily and completely optimize image loading for Gatsby sites.

Using Gatsby image, I can automatically format all images to the WebP file type and serve these on browsers that support this file type. WebP is an image format employing both lossy and lossless compression, along with animation and alpha transparency. Developed by Google, it is designed to create smaller or better-looking images compared to the JPEG, PNG, or GIF image formats. In general, this is the best file format to use when deploying images to your website.

Gatsby image also produces images of varying sizes for each screen size, ensuring that images are loaded appropriately for all screens, maximising the efficiency of the image when loading on smaller screens.

Finally, Gatsby image also has some custom lazy loading behaviour which dramatically increases its efficiency when loading images. In addition to the full resolution image, Gatsby also stores a very low-resolution version for all of your images. When the page loads, a blurred version of this low-resolution image is downloaded, which happens very quickly being a very small file, and is shown to the user. This low-resolution version is shown until the full resolution image has been loaded. This makes for a seamless user experience, with incredibly fast page loading times. Google also considers the page loaded when the low-resolution images appear, massively increasing the performance of your site in the Google indexing algorithm.

To see this in action, scroll to the top and refresh the page, you should see the header image initially appear blurred, before it is replaced by the full resolution image.

gatsby-image is available as an official plugin and documentation of the plugin can be found on the Gatsby website. Below is a simple use case for loading an image using GraphQL and displaying it using a Gatsby Image. Check the docs to learn more about Gatsby images and the differences between fixed and fluid images.

Below is a basic example of using Gatsby image within a component.

import { useStaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"
export default function Image() {
  const data = useStaticQuery(graphql`
    query {
      file(relativePath: { eq: "images/default.jpg" }) {
        childImageSharp {
          # Specify a fluid image and fragment
          # The default maxWidth is 800 pixels
          fluid {
            ...GatsbyImageSharpFluid
          }
        }
      }
    }
  `)
  return (
    <div>
      <h1>Hello gatsby-image</h1>
      <Img
        fluid={data.file.childImageSharp.fluid}
        alt="Gatsby Docs are awesome"
      />
    </div>
  )
}

The future of my website

There is a lot I still want to do with my website, but I think we've got things off to a good start. Now begins the arduous journey of trying to move the pages of the website up Google's rankings. Sitemaps can be automatically generated using a Gatsby plugin, as can a Robots.txt file. Both of these help improve web crawlers understanding of your site. But sadly, there is no magical Gatsby plugin for creating high impact backlinks. That still has to be done manually.

When I get the chance I would like to make the UI a little less static, with some more complex animations. But for now, the amazing and compact library of Sal.js is doing a fantastic job of livening up an otherwise static UI.

I would also like to integrate commenting and other interactions directly into the website, to allow you to provide feedback, as well as allowing viewers to filter my work dependent on interactions. As this is often the most useful way to decipher what content viewers are most interested in.

Of course, as the content on the website grows, I'll have to think about how best to add pagination or infinite scrolls, and whether the latter can even be built using this framework, considering Gatsby builds static HTML pages.

I really enjoyed the process of making this website. I found using Gatsby a seamless developing experience, and the results for the speed of the website were nothing short of astounding. While this framework would not have worked for some of my projects, Bleepr, for example, as the dynamic nature of the content could not be recreated in static HTML pages. It is still an excellent choice for websites that don't require that degree of dynamism.

Thanks for reading to the end! If you have any questions, don't hesitate to get in touch with me on social media, all links to which should be at the top of this page.

And as always, happy devving.