How To Build a Bookstore Landing Page with Gatsby and TypeScript
How To Build a Bookstore Landing Page with Gatsby and TypeScript

Tutorial

How To Build a Bookstore Landing Page with Gatsby and TypeScript

DevelopmentJavaScriptTypeScriptGatsbyJS

The author selected the Diversity in Tech Fund to receive a donation as part of the Write for DOnations program.

Introduction

Landing pages are web pages that promote a product or service, providing a place for customers to land when arriving at a site. For businesses, they often are the destination of links in online advertisments and marketing emails. A commercial landing page’s primary goal is to turn visitors into potential clients or customers. Because of this, building a landing page is a valuable skill for a web developer.

In this tutorial, you will build a landing page with the following two technologies:

  • Gatsby, a React-based frontend framework designed to generate static websites. Gatsby allows you to generate landing pages quickly, which can be useful when creating many landing pages for different projects.

  • TypeScript is a superset of JavaScript that introduces static types and type-checking at build-time. TypeScript has become one of the most widely used alternatives to JavaScript because of its strong typing system, which alerts developers to problems in code before the code gets into proudction. For the landing page, TypeScript will help guard against invalidly typed data for dynamic values, such as the sales pitch text or the signup form input.

The example landing page you will build in this tutorial will promote a bookstore, and will include the following common components of a landing page:

  • A header for the bookstore
  • A hero image that relates to the store
  • A sales pitch with a list of features/services
  • A email signup form that can be used to add the reader to a mailing list about the business

The sample project will look like the following image by the end of the tutorial:

Resulting landing page from following this tutorial, displaying the header, hero image, and sales pitch

Prerequisites

Step 1 — Refactoring the Header and Layout Components

In this step, you will begin by refactoring the existing header.tsx and layout.tsx components of the bookstore-landing-page project that you created in the prerequisite tutorial. This will include replacing default type-definitions with custom-type interfaces and revising some GraphQL queries. Once you have completed this step, you will have populated the header of your landing page with the page’s title and description and created a layout component to implement future components.

The Header component will display the title and description of your page at the top of the browser window. But before refactoring this component, you will open the gatsby-config.js file in the project’s root directory to update the site’s metadata. Later, you will query gatsby-config.js from the Layout component to retrieve this data.

Open gatsby-config.js in your text editor of choice. Under siteMetaData in the exported module, change the value of title and description to the name of the bookstore and a business slogan, as shown in the following highlighted code:

bookstore-landing-page/gatsby-config.js
module.exports = {
  siteMetadata: {
    title: `The Page Turner`,
    description: `Explore the world through the written word!`,
    author: `@gatsbyjs`,
  },
  plugins: [
    ...

After making these changes, save and close the gatsby-config.js file.

Next, inside the bookstore-landing-page/src/components directory, open the header.tsx file. From here you will refactor the <Header /> component to use TypeScript typing instead of the default PropTypes. Make the following changes to your code:

bookstore-landing-page/src/components/header.tsx
import * as React from "react"
import { Link } from "gatsby"

interface HeaderProps {
  siteTitle: string,
  description: string
}

const Header = ({ siteTitle, description }: HeaderProps) => (
  ...
)

export default Header

You deleted the Header.PropTypes and Header.defaultProps objects after the Header declaration and replaced them with a custom-type interface HeaderProps, using the siteTitle and description properties. Then, you added description to the list of arguments passed to the functional component and assigned them to the HeaderProps type. The newly defined HeaderProps interface will act as a custom type for the arguments passed to the <Header/> component from the GraphQL query in the <Layout/> component.

Next, in the JSX of the <Header /> component, change the styling in the opening header tag so the background color is blue and the text is center-aligned. Keep siteTitle in the embedded <Link/> component, but add description to a separate <h3/> tag and give it a font color of white:

bookstore-landing-page/src/components/header.tsx
...

const Header = ({ siteTitle, description }: HeaderProps) => (
  <header
    style={{
      background: `#0069ff`,
      textAlign: `center`,
    }}
  >
    <div
      style={{
        margin: `0 auto`,
        maxWidth: 960,
        padding: `1.45rem 1.0875rem`,
      }}
    >
      <h1 style={{ margin: 0 }}>
        <Link
          to="/"
          style={{
            color: `white`,
            textDecoration: `none`,
          }}
        >
          {siteTitle}
        </Link>
      </h1>
      <h3 style={{
        color: 'white'
      }}>
        {description}
      </h3>
    </div>
  </header>
)

export default Header

Now you will have inline styling when data is passed to this component.

Save the changes in the header.tsx file, then run gatsby develop and go to localhost:8000 on your browser. The page will look like the following:

Landing page with title rendered in header

Notice the description has not yet been rendered. In the next step, you will add this to the GraphQL query in layout.tsx to ensure that it is displayed.

With the <Header/> component ready, you can now refactor the default <Layout/> component for the landing page.

Layout

The <Layout /> component will wrap your landing page, and can help share styles and formatting for future pages on your site.

To start editing this component, open layout.tsx in your text editor. Delete the default type definitions at the end of the file and define a new interface named LayoutProps after the import statements. Then, assign the interface type to the arguments passed to <Layout/>:

bookstore-landing-page/src/components/layout.tsx
/**
 * Layout component that queries for data
 * with Gatsby's useStaticQuery component
 *
 * See: https://www.gatsbyjs.com/docs/use-static-query/
 */

import * as React from "react"
import { useStaticQuery, graphql } from "gatsby"

import Header from "./header"
import "./layout.css"

interface LayoutProps {
  children: ReactNode
}

const Layout = ({ children }: LayoutProps) => {
  ...
}

default export Layout

The interface uses the ReactNode type, which you imported with the React library. This type definition applies to most React child components, which is what <Layout/> renders by default. This will enable you to define a custom-type interface for <Layout/>.

Next, revise the default GraphQL query located inside the <Layout/> component. Inside of the siteMetaData object, add the description that was set in gatsby-config.js. Then, like with siteTitle, store the fetched value in a new description variable:

bookstore-landing-page/src/components/layout.tsx
...

const Layout = ({ children }: LayoutProps) => {
  const data = useStaticQuery(graphql`
    query SiteTitleQuery {
      site {
        siteMetadata {
          title
          description
        }
      }
    }
  `)

  const siteTitle = data.site.siteMetadata?.title || `Title`
  const description = data.site.siteMetadata?.description || 'Description'

 ...

Now you can pass description as a prop to the <Header/> component in the layout’s returned JSX. This is important because description was defined as a required property in the HeaderProps interface:

bookstore-landing-page/src/components/layout.tsx

...

  return (
    <>
      <Header siteTitle={siteTitle} description={description}/>
      ...
    </>
  )

export default Layout

Save and exit from the layout.tsx file.

As a final change to your layout, go into layouts.css to make a styling change by centering all text in the body of the page:

bookstore-landing-page/src/components/layout.css
...

/* Custom Styles */

body {
  margin: 0;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: hsla(0, 0%, 0%, 0.8);
  font-family: georgia, serif;
  font-weight: normal;
  word-wrap: break-word;
  font-kerning: normal;
  -moz-font-feature-settings: "kern", "liga", "clig", "calt";
  -ms-font-feature-settings: "kern", "liga", "clig", "calt";
  -webkit-font-feature-settings: "kern", "liga", "clig", "calt";
  font-feature-settings: "kern", "liga", "clig", "calt";
  text-align: center;
}

...

Save and close the layout.css file, then start the development server and render your site in the browser. You will now find the description value rendered in the header:

Landing page with rendered header but no content

Now that you have refactored the base files for your Gatsby site, you can add a hero image to your page to make it more visually appealing to customers.

Step 2 — Adding a Hero Image

A hero image is a visual that lends support to the product or service in the landing page. In this step, you will download an image for your bookstore landing page and render it on the site using the <StaticImage /> component of the gatsby-plugin-image plugin.

Note: This project is using Gatsby version 3.9.0, so you won’t be able to use the deprecated gatsby-image package. This package was replaced with gatsby-plugin-image. This new plugin, along with help from gatsby-plugin-sharp, will render responsive images with processing functionality.

First, download the bookshelf image from Unsplash, a site that provides images that you can use freely:

  • curl https://images.unsplash.com/photo-1507842217343-583bb7270b66 -o src/images/bookshelf.png

This command uses curl to download the image. The -o flag designates the output, which you have set to be a file named bookshelf.png in the images directory.

Now open the src/pages/index.tsx file. The Gatsby default starter template already has a <StaticImage/> component, so replace the attributes to point to your newly downloaded image:

bookstore-landing-page/src/pages/index.tsx
import * as React from "react"
import { StaticImage } from "gatsby-plugin-image"

import Layout from "../components/layout"
import Seo from "../components/seo"

const IndexPage = () => (
  <Layout>
    <Seo title="Home" />
    <StaticImage
      src="../images/bookshelf.png"
      alt="Bookshelf hero image"
    />
  </Layout>
)

export default IndexPage

You added a src attribute to direct Gatsby to the correct image in your images directory, then added the alt attribute to provide alternative text for the image.

Save and close the file, then restart the development server. Your page will now have the downloaded bookshelf image rendered at its center:

Rendered landing page with bookshelf image.

With your image now rendered on your site, you can move on to adding some content to the page.

Step 3 — Creating a Sales Pitch and Features Component

For the next part of the landing page, you are going to build a new component that holds the sales pitch for your bookstore. This will explain why your customers should come to your store.

Inside of bookstore-landing-page/src/components, go ahead and create a new file titled salesPitchAndFeatures.tsx. Inside the new file, import React, create a new functional component called SalesPitchAndFeatures, and export it:

bookstore-landing-page/src/components/salesPitchAndFeatures.tsx
import * as React from "react"

const SalesPitchAndFeatures = () => {
  <>
  </>
}

export default SalesPitchAndFeatures

The interface for this component will include an optional salesPitch property of type string. It will also have a list of features of type Array<string>, which is required:

bookstore-landing-page/src/components/salesPitchAndFeatures.tsx
import * as React from "react"

interface SalesPitchAndFeaturesProps {
  salesPitch?: string
  features: Array<string>
}
...

The data for the salesPitch and features will be hard-coded within salesPitchAndFeatures.tsx, but you could also store it in another place (like gatsby-config.js) and query the needed data with GraphQL. The content object will be of type SalesPitchAndFeaturesProps:

bookstore-landing-page/src/components/salesPitchAndFeatures.tsx
...

interface salesPitchAndFeaturesProps {
    salesPitch?: string 
    features: Array<string>
}

const content: SalesPitchAndFeaturesProps = {
    salesPitch: "Come and expand your world at our bookstore! We are always getting new titles for you to see. Everything you need is here at an unbeatable price!",
    features: [ 
    "Tens of thousands of books to browse through",
    "Engage with the community at a book club meeting",
    "From the classics to the newest publications, there's something for everybody!"
]}

const SalesPitchAndFeatures = () => {
    return (
        <>

          ...

Notice that the salesPitch prop is a string and the features prop is an array of strings, just as you set them in your interface.

You’ll also need a function that will display the list of features. Create a showFeatures(f)function.

bookstore-landing-page/src/components/salesPitchAndFeatures.tsx
...

const showFeatures: any = (f: string[]) => {
    return f.map(feature => <li>{feature}</li>)
}

const SalesPitchAndFeatures = () => {
    return (
        <>

          ...

The argument f passed into showFeatures is of type Array<string> to be consistent with the array of features of type string. To return the list tranformed into rendered JSX, you use the .map() array method.

Populate the return statement with your content, wrapped in divs with assigned class names for styling:

bookstore-landing-page/src/components/salesPitchAndFeatures.tsx
...

const SalesPitchAndFeatures = () => {
    return (
        <div className='features-container'>
            <p className='features-info'>
                {content.salesPitch}
            </p>
            <ul className='features-list'>
                {showFeatures(content.features)}
            </ul>
        </div>
    )
}

export default SalesPitchAndFeatures

Save and close salesPitchAndFeatures.tsx.

Next, open layout.css to add styling to the class names added in the <SalesPitchAndFeatures/> component:

bookstore-landing-page/src/components/layout.css
...
.features-container {
  border: 1px solid indigo;
  border-radius: 0.25em;
  padding: 2em;
  margin: 1em auto;
}

.features-list {
  text-align: left;
  margin: auto;
}

This adds a border around the sales pitch and features list, then adds spacing between the elements to increase readability.

Save and close layout.css.

Lastly, you will render this component on the landing page. Open index.tsx in the src/pages/ directory. Add the <SalesPitchAndFeatures/> component to the rendered layout children:

bookstore-landing-page/src/pages/index.tsx
import * as React from "react"
import { StaticImage } from "gatsby-plugin-image"

import Layout from "../components/layout"
import SalesPitchAndFeatures from "../components/salesPitchAndFeatures"
import SEO from "../components/seo"

const IndexPage = () => (
  <Layout>
    <SEO title="Home" />
    <div style={{ maxWidth: `450px`, margin: ' 1em auto'}}>
      <StaticImage
        src="../images/bookshelf.png"
        alt="Bookshelf hero image"
      />
      <SalesPitchAndFeatures/>
    </div>
  </Layout>
)

export default IndexPage

You also added in a div to apply some styling to both the image and the sales pitch.

Save and exit from the file. Restart your development server and you will find your sales pitch and features list rendered below your image:

Rendered page with sales pitch and features added

You now have a sales pitch on your landing page, which will help communicate to potential customers why they should go to your business. Next, you will build the final component for the landing page: an email signup form.

Step 4 — Creating a Signup Form Component

An email signup button is a common landing page component that lets the user enter their email address and sign up for more news and information about the product or business. Adding this to your landing page will give the user an actionable step they can take to become your customer.

To start, create a new file in bookstore-landing-page/src/components called signupForm.tsx. This component won’t have any custom types but will have an event handler, which has its own special React-based type.

First, build the <SignUpForm/> component and its return statement, with a header inside:

bookstore-landing-page/src/components/signupForm.tsx
 import * as React from "react"

const SignUpForm = () => {
  return (
    <h3>Sign up for our newsletter!</h3>
  )
}

export default SignupForm

Next, add some markup to create a form element with an onSubmit attribute, initialized to null for now. Soon this will contain the event handler, but for now, finish writing the form with label, input, and button tags:

bookstore-landing-page/src/components/signupForm.tsx
import * as React from "react"

const SignUpForm = () => {
  return (
    <React.Fragment>
      <h3>Sign up for our newsletter!</h3>
      <form onSubmit={null}>
        <div style={{display: 'flex'}}>
            <input type='email' placeholder='email@here'/>

        <button type='submit'>Submit</button>
        </div>
      </form>
    <React.Fragment>
  )
}

export default SignupForm

If you type your email address in the text field and click Sign Up on the rendered landing page right now, nothing will happen. This is because you still need to write an event handler that will trigger whenever the form is submitted. A best practice is to write a separate function outside the return statement for the event handler. This function usually has a special e object that represents the triggered event.

Before writing the separate function, you will write the function in-line to figure out what the static type of the event object is, so that you can use that type later:

bookstore-landing-page/src/components/signupForm.tsx
...

    return (
        <React.Fragment>
            <h3>Sign up for our newsletter!</h3>
            <form onSubmit={(e) => null}>
                <div style={{display: 'flex'}}>
                    <input type='email' placeholder='email@here' style={{width: '100%'}}/>
                    <button type="submit">Submit</button>
                </div>
            </form>
        </React.Fragment>
    )
...
}

export default SignupForm

If you are using a text editor like Visual Studio Code, hovering your cursor over the e parameter will use TypeScript’s IntelliSense to show you its expected type, which in this case is React.FormEvent<HTMLFormElement>.

Now that you know what the expected type of your separate function will be, go ahead and use this to write a new, separate function called handleSubmit:

bookstore-landing-page/src/components/signupForm.tsx
import * as React from "react"

const SignupForm = () => {
    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        alert(alert('The submit button was clicked! You\'re signed up!'))
    }
    return (
        <React.Fragment>
            <h3>Sign up for our newsletter!</h3>
            <form onSubmit={handleSubmit}>
                <div style={{display: 'flex'}}>
                  <input type='email' placeholder='email@here'/>
                  <button type='submit'>Submit</button>
                </div>
            </form>
        </React.Fragment>
  )
}

export default SignupForm

The handleSubmit function will now trigger a browser alert when the form is submitted. Note that this is a temporary placeholder; to actually add the user to an email list, you would have to connect this to a back-end database, which is beyond the scope of this tutorial.

Save and close the signupForm.tsx file.

Now, open the index.tsx file and add the new <SignupForm/> component:

bookstore-landing-page/src/pages/index.tsx
import * as React from "react"
import { StaticImage } from "gatsby-plugin-image"

import Layout from "../components/layout"
import SalesPitchAndFeatures from "../components/salesPitchAndFeatures"
import SignupForm from "../components/signupForm"
import Seo from "../components/seo"

const IndexPage = () => (
  <Layout>
    <Seo title="Home" />
    <div style={{ maxWidth: `450px`, margin: ' 1em auto'}}>
      <HeroImage />
      <SalesPitchAndFeatures />
      <SignupForm />
    </div>
  </Layout>
)

export default IndexPage

Save and exit the file.

Restart your development server, and you will find your completed landing page rendered in your browser, along with the email signup button:

Email signup button rendered below the feature list on the landing page.

You have now finished building all the core components of your bookstore landing page.

Conclusion

Because Gatsby creates fast static websites and TypeScript allows for data to be statically typed, building a landing page makes for an excellent use case. You can shape the types of its common elements (header, hero image, email signup, etc.) so that incorrect forms of data will trigger errors before they go into production. Gatsby provides the bulk of the structure and styling of the page, allowing you to build on top of it. You can use this knowledge to build other landing pages to promote other products and services quicker and more efficiently.

If you’d like to learn more about TypeScript, check out our How To Code in TypeScript series, or try our How To Create Static Web Sites with Gatsby.js series to learn more about Gatsby.

Creative Commons License