You've learned how to add a Tailwind component to an existing theme, and you've seen how flexible sectioned page builder models give themes empower content authors to create beautiful web sites without requiring additional developer time. Let's put the two concepts together and add sectioning to a Tailwind theme where it didn't exist.

Follow along with this Gatsby + Contentful + Tailwind starter. You can create it in Stackbit to start right away.

Step 1: Design your sections

In the Gatsby theme to which we previously added a Tailwind UI component, the site's Home page was a hard-coded /src/pages/index.js file:

// From index.js

<Hero />

<div className="bg-gray-100 py-12 lg:py-16">
  {data.portfolio && data.portfolio.nodes.length - 0 ? (
    <Cards items={data.portfolio.nodes} />
  ) : (<div className="container">No projects found.</div>)}
</div>

<Newsletter />

Let's replace this theme's home page with a new Gatsby page template, powered by a new Contentful content type called "Flexible Page."

Screenshot of Home, outlined as sections

On the Gatsby side, "Home" is already pretty well-divided into <Hero/>, <Cards/>, and <Newsletter> components, but they need to be refactored to use Contentful data rather than hard-coded strings such as "Hello, I'm John" and "Enter your email."

To truly fit into a sectioned page builder model, even the option to show <Cards/> on a given page needs to exist as a Contentful content type. Maybe it doesn't have any siginificant fields -- just a "yes, show me" Boolean -- but the content type needs to exist.

Step 2: Create new content types on Contentful

Content types

Now that the conceptual modeling is done, head to Contentful and create 6 new content types: Hero, Portfolio Teaser, Newsletter (to section the existing page), Features List, Feature Item (to add a Tailwind UI component), and Flexible Page (to tie everything together):

Diagram of content type relationships

Hero

  1. "Hello, I'm John" → Title
  2. "Welcome to my photography portfolio." → Subtitle

Portfolio Teaser

  1. Show?(Boolean)

Newsletter

  1. "Sign up for my newsletter" → Title
  2. "Enter your email" → Email Prompt
  3. "Sign up" → Button Text

Feature Item

  1. "Competitive exchange rates", "No hidden fees", etc. → Title
  2. Various "Lorem ipsum..." → Subtitle
  3. Various purple SVG graphics → Image

Features List

  1. "Transactions" → Tagline
  2. "A better way to send money" → Title
  3. "Lorem ipsum ... quisquam" → Subtitle
  4. A reference field set to "many references," restricted to "Feature Item" content → Features.

Flexible Page

  1. Title
  2. Slug
  3. A reference field set to "many references," restricted to "Features List," "Hero," "Newsletter," and "Portfolio Teaser" content → Sections

Screenshot of a multi-reference array in Contentful

Initial data

Create at least one piece of content for each of your new Contentful content types.

When you create a Flexible Page, title it "Home", and give it a slug of "/". Fill its sections field with content that approximates the material that was previously hard-coded -- and add a Tailwind UI component section, for good measure.

Screenshot of all data in Contentful

Step 3: Update your Gatsby theme

Hello world

First, using the Stackbit code editor, create a new Gatsby template file called src/templates/flexible-page.jsx

Screenshot of adding a file to a folder in the Stackbit code editor

If you prefer, you can edit the preview branch of the GitHub repository Stackbit is managing for you through your favorite development workflow.

Here's how flexible-page.jsx should look (we'll add to it later):

// src/templates/flexible-page.jsx
import { graphql } from 'gatsby';
import React from 'react';
import SiteMetadata from '../components/SiteMetadata';
import Layout from '../layouts/Layout';

export default (props) => {
  const { title, slug } = props.data.item;

  return (
    <Layout>
      <SiteMetadata title={title} />
      <h1>
        Hello {title} - {slug}
      </h1>
    </Layout>
  );
};

export const query = graphql`
  query FlexiblePageQuery($slug: String!) {
    item: contentfulFlexiblePage(slug: { eq: $slug }) {
      title
      slug
    }
  }
`;

Second, replace the contents of gatsby-node.js as follows:

// gatsby-node.js
const path = require(`path`);
const trim = (str, c = '\\s') => str.replace(new RegExp(`^([${c}]*)(.*?)([${c}]*)$`), '$2');

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions;
  const typeDefs = `
    type contentfulPortfolioDescriptionTextNode implements Node {
      description: String
    }
    type ContentfulPortfolio implements Node {
      description: contentfulPortfolioDescriptionTextNode
      gallery: [ContentfulAsset]
      id: ID!
      name: String!
      related: [ContentfulPortfolio]
      slug: String!
      summary: String!
      thumbnail: ContentfulAsset
      url: String
    }
  `;
  createTypes(typeDefs);
};

exports.createPages = ({ graphql, actions }) => {
  const { createPage } = actions;

  return new Promise((resolve, reject) => {
    graphql(`
      {
        portfolio: allContentfulPortfolio {
          nodes {
            slug
          }
        }
        flexible: allContentfulFlexiblePage {
          nodes {
            slug
          }
        }
      }
    `).then(({ errors, data }) => {
      if (errors) {
        reject(errors);
      }

      if (data) {
        if (data.portfolio) {
          const component = path.resolve('./src/templates/portfolio-item.jsx');
          data.portfolio.nodes.map(({ slug }) => {
            createPage({
              path: `/${trim(slug, '/')}`,
              component,
              context: { slug }
            });
          });
        }
        if (data.flexible) {
          const component = path.resolve('./src/templates/flexible-page.jsx');
          data.flexible.nodes.map(({ slug }) => {
            createPage({
              path: `/flex/${trim(slug, '/')}`,
              component,
              context: { slug }
            });
          });
        }
      }

      resolve();
    });
  });
};

Note that we trim leading & trailing / values off of the slug in gatsby-node.js under exports.createPages.

We won't need to worry about this for stackbit.yaml -- it's clever enough to figure out how to handle the home page's slug of /.

Also note that we're starting by putting our flexible pages under a sub-folder called /flex, to avoid conflicts with existing hard-coded Gatsby pages. We'll change that later once we know everything's working and delete the hard-coded pages.

Third, add the following lines of code to the end of stackbit.yaml (indent it at the same level that portfolio above it is indented):

flexiblePage:
  type: page
  urlPath: '/flex/{slug}'

Let the Stackbit preview engine restart.

Sometimes, Gatsby can't quite keep up with your changes, even if your code is perfect. If you run into problems that don't make sense, try refreshing your browser page. If that doesn't help, restart the Stackbit preview engine:

  1. Click the settings gear icon in the upper left corner
  2. Click its Advanced tab at the upper left of the pop-up
  3. Click Restart in the lower right corner of the popup

Screenshot of the Stackbit restart preview button

In Stackbit, leave the Code tab you've been working with and make your pages editable by clicking the Content tab at the top center.

Toward the top of the left side navbar, click the caret next to the word Page to expand a site navigation menu.

  1. Below the search box, verify that you see items reading /flex, /iceland, /poland, and /spain.
  2. Click on /flex. Verify that the page's preview says Hello Home - /.

Screenshot of the early flexible pages

Screenshot of the early new home page

At the top of the left side navbar click Add page, then Create Page. Under Choose a template, select Flexible Page, and under New page path, type contact and click Create.

Verify that Stackbit takes you to your new page and that its preview says Hello lorem-ipsum - contact.

Screenshot of the new Contact page in Stackbit

If you have Contentful open, you'll see it appear as a draft.

Screenshot of the new Contact page visible in Contentful

Feel free to use Stackbit to edit the title of any test pages you create, so that you don't have to keep track of lorem-ipsums.

Screenshot of an edited title in Stackbit

Major edits

Now that you've successfully added a new page type to Gatsby, go back to Stackbit's code editor and create or replace the following files with these contents:

Edit src/components/Hero.jsx

// src/components/Hero.jsx

// JSX simplified for readability

import { graphql } from 'gatsby';
import PropTypes from 'prop-types';
import React from 'react';

const Hero = (props) => {
  const { title, subtitle } = props;
  return (
    <div className="container">
      <h2 className="text-4xl">
        {title}
        <br />
        <span className="text-blue-600">{subtitle}</span>
      </h2>
    </div>
  );
};

Hero.propTypes = {
  title: PropTypes.string.isRequired,
  subtitle: PropTypes.string.isRequired
};

export default Hero;

export const query = graphql`
  fragment HeroFragment on ContentfulHero {
    title
    subtitle
  }
`;

If the now-blank hero section on src/pages/index.js bothers you, replace <Hero /> with <Hero title="Hi 👋" subtitle="Welcome" />.

☝️ Note that we lost the accessibility we'd had with the hard-coded role and aria-label surrounding our emoji.

<span role="img" aria-label="waving hand">
  👋
</span>

Be sure to incorporate an emoji accessibility solution into your theme, e.g. by using a plugin such as @fec/remark-a11y-emoji, so you can surround {title} in Hero.jsx with code that will detect emojis and surround them with proper HTML.

Create src/components/PortfolioTeaser.jsx

// src/components/PortfolioTeaser.jsx

// JSX simplified for readability

import { StaticQuery, graphql } from 'gatsby';
import PropTypes from 'prop-types';
import React from 'react';
import Cards from './Cards';

export default function PortfolioTeaser() {
  return (
    <StaticQuery
      query={the_query}
      render={(data) => {
        const the_teaser = (
          <div className="bg-gray-100">
            {data.portfolio && data.portfolio.nodes.length > 0 ? (
              <Cards items={data.portfolio.nodes} />
            ) : (
              <div className="container">No projects found.</div>
            )}
          </div>
        );
        return !!data.singleTeaser.show ? the_teaser : null;
      }}
    />
  );
}

const the_query = graphql`
  query PortfolioTeaserQuery {
    portfolio: allContentfulPortfolio {
      nodes {
        ...PortfolioCard
      }
    }
    singleTeaser: contentfulPortfolioTeaser {
      show
    }
  }
  fragment PortfolioTeaserFragment on ContentfulPortfolioTeaser {
    show
  }
`;

Edit src/components/Newsletter.jsx

First, replace this:

const Newsletter = () => {
  const [email, setEmail] = useState()

With this:

const Newsletter = props => {
    const { title, emailPrompt, buttonText } = props;
    const [email, setEmail] = useState()

Second, replace Sign up for my newsletter, "Enter your email", and Sign up with {title}, {emailPrompt}, and {buttonText}, respectively.

Third, add the following GraphQL fragment to the end of the file:

export const query = graphql`
  fragment NewsletterFragment on ContentfulNewsletter {
    title
    emailPrompt
    buttonText
  }
`;

As with the Hero section, if you find the blank newsletter prompt in index.jsx disconcerting, replace <Newsletter /> with <Newsletter title="Sign up now" emailPrompt="Email here" buttonText="Submit email" />.

Create src/components/FeatureItem.jsx

// src/components/FeatureItem.jsx

// JSX simplified for readability and to respect Tailwind UI licensing
import { graphql } from 'gatsby';
import PropTypes from 'prop-types';
import React from 'react';

const FeatureItem = (props) => {
  const { title, subtitle, image } = props;
  return (
    <div className="relative">
      <dt>
        <div className="absolute h-6 w-6 text-blue-600" dangerouslySetInnerHTML={{ __html: image.svg.content }} />
        <p className="ml-8 text-xl text-black">{title}</p>
      </dt>
      <dd className="ml-8 text-gray-700">{subtitle}</dd>
    </div>
  );
};

FeatureItem.propTypes = {
  title: PropTypes.string.isRequired,
  subtitle: PropTypes.string.isRequired,
  image: PropTypes.object.isRequired
};

export default FeatureItem;

export const query = graphql`
  fragment FeatureItemFragment on ContentfulFeatureItem {
    id
    title
    subtitle
    image {
      svg {
        content
      }
    }
  }
`;

(Note: you need to install the gatsby-transformer-inline-svg Gatsby plugin for image.svg.content to be available to your GraphQL fragment.)

Create src/components/FeaturesList.jsx

// src/components/FeaturesList.jsx

// JSX simplified for readability and to respect Tailwind UI licensing
import { graphql } from 'gatsby';
import PropTypes from 'prop-types';
import React from 'react';
import FeatureItem from './FeatureItem';

const FeaturesList = (props) => {
  const { tagline, title, subtitle, features } = props;
  return (
    <div className="px-4">
      <div>
        <h2 className="text-blue-600 uppercase">{tagline}</h2>
        <p className="text-4xl font-extrabold">{title}</p>
        <p className="text-gray-700">{subtitle}</p>
      </div>

      <dl>
        {features.map((featureItem) => (
          <FeatureItem {...featureItem} />
        ))}
      </dl>
    </div>
  );
};

FeaturesList.propTypes = {
  tagline: PropTypes.string.isRequired,
  title: PropTypes.string.isRequired,
  subtitle: PropTypes.string.isRequired
};

export default FeaturesList;

export const query = graphql`
  fragment FeaturesListFragment on ContentfulFeaturesList {
    id
    tagline
    title
    subtitle
    features {
      ...FeatureItemFragment
    }
  }
`;

Edit src/templates/flexible-page.jsx

// src/templates/flexible-page.jsx

import { graphql } from 'gatsby';
import React from 'react';
import SiteMetadata from '../components/SiteMetadata';
import Layout from '../layouts/Layout';
import Hero from '../components/Hero.jsx';
import FeaturesList from '../components/FeaturesList.jsx';
import Newsletter from '../components/Newsletter.jsx';
import PortfolioTeaser from '../components/PortfolioTeaser.jsx';

const sectionComponentTypes = {
  Hero,
  FeaturesList,
  Newsletter,
  PortfolioTeaser
};

export default (props) => {
  const { title, slug, sections } = props.data.item;

  const sectionComponents = !!sections
    ? sections.map((section) => {
        const componentTypeName = section['__typename'].replace(/^Contentful/, '');
        let Component = sectionComponentTypes[componentTypeName];
        return <Component {...section} />;
      })
    : null;

  return (
    <Layout>
      <SiteMetadata title={title} />
      {sectionComponents}
      <div id="editor_shim" className="hidden">
        {title}
      </div>
    </Layout>
  );
};

export const query = graphql`
  query FlexiblePageQuery($slug: String!) {
    item: contentfulFlexiblePage(slug: { eq: $slug }) {
      title
      slug
      sections {
        ... on ContentfulHero {
          __typename
          ...HeroFragment
        }
        ... on ContentfulFeaturesList {
          __typename
          ...FeaturesListFragment
        }
        ... on ContentfulNewsletter {
          __typename
          ...NewsletterFragment
        }
        ... on ContentfulPortfolioTeaser {
          __typename
          ...PortfolioTeaserFragment
        }
      }
    }
  }
`;

Now that you've edited the Gatsby theme, leave the Code tab by clicking the Content tab again, expand the caret by Page at left to enter site navigation, and click on /flex.

The page should look a lot like the original home page. (Visually, you might want to touch up the JSX in a few components, since the copy-paste examples in this guide were simplified for readability and to respect licensing.)

Screenshot of the new home page looking proper

You can use Stackbit to add, remove, and rearrange sections. Try navigating to Contact (under /flex in the site nav at left) and adding some sections.

Final touches

Back in the Code tab:

  1. Erase /flex from stackbit.yaml so that the urlPath for flexiblePage is '/{slug}'
  2. Delete the /src/pages/index.js file
  3. Erase /flex fom gatsby-node.js so that the path for the appropriate createPage call is /${trim(slug, '/')}
  4. Let Stackbit restart Gatsby.

Expanding the caret by Page at left to enter site navigation, you should now see /, /contact, /iceland, /poland, and /spain.

Screenshot of the Stackbit site nav after eliminating "flex" from the URL

If you'd like to add Contact to the site's top navigation bar, replace these lines of gatsby-config.js:

module.exports = {
  siteMetadata: {
    menu: [
      { name: "Home", to: "/" },
      { name: "About", to: "/about" },
    ],

With these:

module.exports = {
  siteMetadata: {
    menu: [
      { name: "Home", to: "/" },
      { name: "About", to: "/about" },
      { name: "Contact", to: "/contact" },
    ],

Now that it's so easy for content authors to add pages that use flexible Tailwind components to this site, a great followup project could be refactoring this theme to manage the navigation menu as CMS data, rather than as part of gatsby-config.js.

Screenshot of the home page in Stackbit Studio, with Contact in the navigation

Incorporating Tailwind components into a Jamstack theme enables end-users to be creative and effective, quickly spinning up pages to fit a variety of impromptu needs. Stackbit provides immediate visual feedback reflecting their work results, empowering everyone to make modern websites an integral part of their strategy.

Helpful resources