Sectioning an existing theme for components
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.”
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):
Hero
- “Hello, I’m John” →
Title
- “Welcome to my photography portfolio.” →
Subtitle
Portfolio Teaser
Show?
→ (Boolean)
Newsletter
- “Sign up for my newsletter” →
Title
- “Enter your email” →
Email Prompt
- “Sign up” →
Button Text
Feature Item
- “Competitive exchange rates”, “No hidden fees”, etc. →
Title
- Various “Lorem ipsum…” →
Subtitle
- Various purple SVG graphics →
Image
Features List
- “Transactions” →
Tagline
- “A better way to send money” →
Title
- “Lorem ipsum … quisquam” →
Subtitle
- A reference field set to “many references,” restricted to “Feature Item” content →
Features
.
Flexible Page
Title
Slug
- A reference field set to “many references,” restricted to “Features List,” “Hero,” “Newsletter,” and “Portfolio Teaser” content →
Sections
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.
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
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:
- Click the settings gear icon in the upper left corner
- Click its Advanced tab at the upper left of the pop-up
- Click Restart in the lower right corner of the popup
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.
- Below the search box, verify that you see items reading
/flex
,/iceland
,/poland
, and/spain
. - Click on
/flex
. Verify that the page’s preview saysHello Home - /
.
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
.
If you have Contentful open, you’ll see it appear as a draft.
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.
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.)
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:
- Erase
/flex
fromstackbit.yaml
so that theurlPath
forflexiblePage
is'/{slug}'
- Delete the
/src/pages/index.js
file - Erase
/flex
fomgatsby-node.js
so that thepath
for the appropriatecreatePage
call is/${trim(slug, '/')}
- Let Stackbit restart Gatsby.
Expanding the caret by Page at left to enter site navigation, you should now see /
, /contact
, /iceland
, /poland
, and /spain
.
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
.
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
- Not on Stackbit yet? Bring your site
- Need a deeper dive on Tailwind? Add a Tailwind component to an existing theme