This is part 4 of our 4 part series. Check out part 1 to get an introduction to TakeShape and learn content modeling and content creation. In part 2, you’ll learn how to create a static site that loads data from TakeShape. In part 3, you’ll learn how to quickly deploy your new static site onto Netlify for a straightforward, secure, and speedy hosting solution.
TLDR: Short-circuit this tutorial and start playing around with the completed site. We won’t be offended! Login and create a new project with “Shape Portfolio” template. Then clone the completed project repo, check out the README.md
and start jamming.
> git clone https://github.com/takeshape/takeshape-samples
> cd takeshape-samples/shape-portfolio-gatsbyjs
The Bold And Adventurous Should Continue
You’ve reached the final part of this series, which showed you how to create a static-site portfolio with TakeShape. If you’ve been following along from the start you’re achieved quite a lot! You’ve created a content model and GraphQL API using TakeShape, used TakeShape’s static site generator to load your content into a fully customizable static site, and deployed your static site with Netlify.
In this conclusion, you’re going to take your portfolio site to the next level by learning how to build it with Gatsby.js, a static site generator that layers modern web tools like React.js and Webpack on top of the speedy and secure foundation of a JAMstack website. Gatsby is another great JAMstack tool for generating a JAMstack site with data from GraphQL APIs, especially when it comes to highly-interactive web applications. And TakeShape makes for a wonderful CMS backend for all of your Gatsby sites since every TakeShape project has a GraphQL API that easily integrates with Gatsby.
In this walkthrough, you’ll get started with Gatsby and translate your existing portfolio website into a React-based website. This is the most advanced walkthrough in this series, so if you haven’t already read through the previous articles, we recommended that you at least skim them first.
Hello Gatsby!
Get started by cloning the Gatsby starter project “hello world” and making sure everything works. Install the required libraries and then run the developer site with npm start
.
> git clone https://github.com/gatsbyjs/gatsby-starter-hello-world shape-portfolio-gatsbyjs
> cd shape-portfolio-gatsbyjs
> npm install
> npm start
Visit localhost:8000
in your browser to see the starter site running!
Connect Gatsby to TakeShape
Since they’re both built around GraphQL data, Gatsby and TakeShape fit together like Lego bricks. After you provide TakeShape as a datasource to Gatsby, you’ll be able to query your data right from your pages and components.
To start, install the gatsby-source-graphql
and dotenv
plugins:
> npm install gatsby-source-graphql dotenv
Then create a gatsby-config.js
file in the project’s root to configure the plugin:
require("dotenv").config();
module.exports = {
plugins: [
{
resolve: "gatsby-source-graphql",
options: {
typeName: "TS",
fieldName: "takeshape",
// Url to query from
url: `https://api.takeshape.io/project/${process.env.TAKESHAPE_PROJECT}/graphql`,
// HTTP headers
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.TAKESHAPE_TOKEN}`,
},
// Additional options to pass to node-fetch
fetchOptions: {},
},
},
]
};
You’ll notice that this configuration uses TAKESHAPE_PROJECT
and TAKESHAPE_TOKEN
values from our local environment. We’ll keep these values in our environment so that we don’t inadvertently commit these secrets to version control.
The Gatsby starter project should already contain a .gitignore
file that includes .env
, but you might want to double check, just in case.
Then, create a read-only TakeShape API token and copy your project’s ID from its URL in the TakeShape web client. Finally, create a file named .env
in the root of your project that looks like this:
TAKESHAPE_PROJECT=<YOUR-PROJECTS-ID>
TAKESHAPE_TOKEN=<YOUR-API-TOKEN>
Test your connection
Start your Gatsby development server with gatsby develop
. As long as the configuration is correct, you’ll see the server start successfully and be able to test your connection to TakeShape with the built-in GraphiQL IDE.
Once the server has started, navigate to http://localhost:8000/___graphql
in your browser. You’ll see a page that looks like the API Explorer in TakeShape—because it’s the same software!
Test that you can access your portfolio projects through Gatsby with this GraphQL query:
query {
takeshape {
getProjectList {
total
}
}
}
As you type this in, you should see the different fields autocomplete. Then, when you run the query you should see the total number of portfolio projects you’ve created in TakeShape so far. As you can see, Gatsby can easily talk to TakeShape, figure out the schema for your entire project, and let you pull that data right into your Gatsby project.
Now, we can start recreating our portfolio in React!
Migrating from Nunjucks to React
We’ll start by creating pages for each project in your portfolio, then move onto the simpler task of recreating the home and about pages.
But before we do any of that, we need to migrate the base layout and structure over to React!
Layout
First we’ll create a folder named layouts
within the src
directory of our project. Then we’ll create a file named default.js
. You can do this in two commands, from the root of your project:
> mkdir src/layouts && touch src/layouts/default.js
Our default.js
layout will be very similar in purpose to the default.html
layout in the Nunjucks template you created for your shape-portfolio
in part 2, though since it’s built with React, it’ll look slightly different:
import React from "react";
import PropTypes from "prop-types";
import Header from "../components/header";
import Footer from "../components/footer";
import Menu from "../components/menu";
import "./default.css";
const Layout = ({children}) => (
<>
<Header siteTitle="Shape Portfolio"/>
<div className="container">
<Menu/>
<div className="main">{children}</div>
<Footer/>
</div>
</>
);
Layout.propTypes = {
children: PropTypes.node.isRequired
};
export default Layout;
The purpose of this component is to provide a basic layout from which all your pages can inherit. By wrapping your other components with this layout, you can ensure that all the pages in your project maintain a consistent appearance and don’t duplicate any functionality. You’ll make children
a required property of the Layout component, since it should always be acting as a wrapper for something else, whether that’s a list of projects, a project itself, or something else entirely.
Next create a default.css
file in the same src/layouts
directory. You’ll notice that it gets imported right into this default.js
file as well. Now you might be saying, “Importing CSS into JS? That’ll never work!” Well since Gatsby comes with Webpack built in, it’ll extract and bundle all your CSS when it’s time to build your site. This has the benefit of letting you keep your styles right alongside your components.
For now, go ahead and copy the main.css
styles from the shape-portfolio
project into default.css
. You won’t be diving into advanced styling in this walkthrough, although the fancier CSS-in-JS techniques that Gatsby supports with additional plugins are worth exploring in more depth.
Header & Footer
In your default.js
layout, the Header, Footer, and Menu are all abstracted into standalone components. You’ll start by creating the Header file, which will provide all the the <head>
tags for your project.
Start by adding the react-helmet
and gatsby-plugin-react-helmet
packages to your project:
> npm install react-helmet gatsby-plugin-react-helmet
Then add gatsby-plugin-react-helmet
to your list of plugins in your gatsby-config.js
file:
module.exports = {
plugins: [
'gatsby-plugin-react-helmet',
{
resolve: "gatsby-source-graphql",
…
}
]
}
React Helmet is an easy-to-use component for managing the document head of all of your pages. It will append the <head>
tags you provide to the beginning of every page you include it on—and since you’re including it in your default layout, it’ll be included in every page.
In your src
directory, create a new components
directory and inside it create a new file named header.js
that looks like this:
import React from "react";
import Helmet from "react-helmet";
const Header = ({siteTitle}) => (
<Helmet>
<html lang="en"/>
<title>{siteTitle}</title>
<meta charSet="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link
href="https://fonts.googleapis.com/css?family=Work+Sans:400,500,600,700"
rel="stylesheet"
/>
</Helmet>
);
export default Header;
That’s all it takes to create a Header component, to be imported and used by your default layout.
Next create your site’s footer in src/components/footer.js
. This component is simpler:
import React from "react";
const Footer = () => (
<footer className="footer">
<p>
© {new Date().getFullYear()}{" "}
<a
href="https://takeshape.io"
target="_blank"
rel="noopener noreferrer nofollow"
>
TakeShape Inc.
</a>
</p>
</footer>
);
export default Footer;
You’ll notice that this footer contains our first piece of dynamic content: the copyright date will automatically reflect the current year by using the new Date().getFullYear()
function. The ability to use JavaScript inside of components is one of the most powerful aspects of React!
Menu
The final layout component that you’ll create will also be the first one to query data from your TakeShape project. To enable this functionality, you’ll use the StaticQuery
API provided by Gatsby. With StaticQuery
, a component can individually request the data that it needs to render. This is a great fit for your Menu component, since it’ll render the same on every page regardless of what other content the page contains.
In order to render images from TakeShape, you’ll also use the getImageUrl
API provided by TakeShape’s takeshape-routing
package.
First, install the takeshape-routing
package:
> npm install takeshape-routing
Then, create a src/components/menu.js
component that looks like this:
import React from "react";
import { Link, StaticQuery, graphql } from "gatsby";
import { getImageUrl } from "takeshape-routing";
import routes from "../routes";
const menuQuery = graphql`
query {
takeshape {
projects: getProjectList {
items {
name
}
}
about: getAbout {
socialProfiles {
profileUrl
socialNetwork
socialNetworkIcon {
path
}
}
}
}
}
`;
const Menu = ({ data }) => (
<nav className="menu">
<div className="menu__container">
<p className="site-name">
<Link to="/">
<strong>Shape Portfolio</strong>
</Link>
</p>
<ul className="nostyle">
{data.takeshape.projects.items.map((project, i) => (
<li key={i}>
<Link to={routes.project(project.name)}>{project.name}</Link>
</li>
))}
</ul>
<hr />
<Link to="/about">About</Link>
<ul className="nostyle inline">
{data.takeshape.about.socialProfiles.map((profile, i) => (
<li className="social-profile" key={i}>
<a
href={profile.profileUrl}
target="_blank"
rel="noopener noreferrer"
>
<img
className="social-network-icon"
src={getImageUrl(profile.socialNetworkIcon.path)}
alt="Social network icon"
/>
<span className="social-network-name">
{profile.socialNetwork}
</span>
</a>
</li>
))}
</ul>
</div>
</nav>
);
export default () => (
<StaticQuery query={menuQuery} render={data => <Menu data={data} />} />
);
The menu consists of two objects: a query for the data we want to include and a component to render it. At the bottom of the file, the two objects get passed to Gatsby’s StaticQuery
API, which takes on the work of fetching the data and rendering the component.
The menu also makes use of Gatsby’s Link
component. Notice how you use Link
to link within your site, as you would link to a project page, while you use a standard anchor to link outside your site, as you would link to a social network. When you use Link
, Gatsby can load these new pages dynamically with Javascript, preventing additional page loads. It’s a slick out-of-the-box feature.
Finally, the Link
uses the function routes.project
to set its destination. You’ll need to create that function yourself. Luckily, that’s the next step!
Project Pages
You’ll want to create a standalone page for every project, so that you can link directly to different projects in your portfolio. But, each page should also be created dynamically, based on data from TakeShape. So how can you go about creating pages dynamically?
First, create a new file named gatsby-node.js
in your site’s root directory. This will contain code that’s executed by Gatsby’s build process. You’ll plug-in to Gatsby’s createPages
API to create a standalone page for each of your portfolio’s projects.
First create the file:
> touch gatsby-node.js
Then add your function:
const path = require("path");
const routes = require("./src/routes");
exports.createPages = async ({ actions, graphql }) => {
const { data } = await graphql(`
query {
takeshape {
projects: getProjectList {
items {
_id
name
}
}
}
}
`);
data.takeshape.projects.items.forEach(({ _id, name }) => {
actions.createPage({
path: routes.project(name),
component: path.resolve("./src/components/Project.js"),
context: {
projectId: _id
}
});
});
};
This function will first query TakeShape for a list of all the project names and IDs. Then, for each project, you’ll use the actions.createPage
API to create its standalone page. Each page will need three things:
- A path, which you’ll generate using a routing function using the project’s name.
- A component, which you’ll create to render the project’s page.
- A context, which you’ll provide to each project page’s individual GraphQL query,
Creating your Project path
You’ll create the function to generate a route for the project. First create a file named routes.js
in the src
directory and add the slugify
package to your project.
> touch src/routes.js
> npm install slugify
Next, in src/routes.js
, add a function for creating routes for your projects based on their names:
const slugify = require("slugify");
exports.project = function(name) {
const slug = slugify(name.toLowerCase());
return `/projects/${slug}/`;
};
As you add other dynamically-generated pages to your portfolio, you can add their routing rules to this same file.
Creating your Project component and query
Your component will render the entire project, which you’ll wrap in the Layout
component you already created. This will render a complete page for each project!
Start by creating a file named Project.js
in your src/components
directory that looks like this:
import React from "react";
import { graphql } from "gatsby";
import { getImageUrl } from "takeshape-routing";
import Layout from "../layouts/default";
export const ProjectMetadata = ({ startDate, endDate, client }) => {
const startYear = new Date(startDate).getFullYear();
const endYear = endDate ? new Date(endDate).getFullYear() : null;
const includeEndYear = endYear && startYear !== endYear;
return (
<div className="project__metadata">
<p className="project__metadata">
{startYear}
{includeEndYear ? `&endash; ${endYear}` : ""}
</p>
{client ? (
<p>
<a href={client.url} target="_blank" rel="noopener noreferrer">
{client.name}
</a>
</p>
) : (
""
)}
</div>
);
};
export const Project = ({ project }) => (
<article className="project">
<header>
{project.coverImage ? (
<img
className="project__cover-image"
alt={project.coverImage.description}
src={getImageUrl(project.coverImage.path, {
h: 600,
w: 1200,
fit: "crop"
})}
/>
) : (
""
)}
<h1>{project.name}</h1>
<ProjectMetadata
startDate={project.startDate}
endDate={project.endDate}
client={project.client}
/>
</header>
<div
className="project__description"
dangerouslySetInnerHTML={{ __html: project.description }}
/>
</article>
);
export default ({ data }) => (
<Layout>
<Project project={data.takeshape.project} />
</Layout>
);
export const query = graphql`
query($projectId: ID!) {
takeshape {
project: getProject(_id: $projectId) {
name
startDate
endDate
coverImage {
description
path
}
client {
name
url
}
description: descriptionHtml
}
}
}
`;
Starting from the bottom, this file exports a query for an individual project and component that renders the complete project page, both of which are used by the createPage
API. The query accepts the context
you provided to the createPage
API and fetches data for each Project page. It then provides it to the page with the data
parameter. The page wraps the Project component in the Layout component in order to render the complete page.
The Project
component in this file provides the logic for actually rendering each project. It also uses a ProjectMetadata
subcomponent to handle the logic for formatting the project’s metadata.
Homepage
Your homepage will provide a list of your projects, which we’ll create in a separate reusable component. The homepage will provide its own query for a list of projects, then pass it on to the ProjectList
.
The Gatsby starter project you used will already contain a file named index.html
in the src/pages
directory. src/pages
is a special directory in Gatsby: each file in this directory will automatically create its own standalone page, as long as the file exports at least a component.
Update your src/pages/index.html
to be like this:
import React from "react";
import { graphql } from "gatsby";
import Layout from "../layouts/default";
import ProjectList from "../components/ProjectList";
const IndexPage = ({ data }) => (
<Layout>
<ProjectList projects={data.takeshape.projects} className="main" />
</Layout>
);
export default IndexPage;
export const query = graphql`
query {
takeshape {
projects: getProjectList {
items {
name
startDate
coverImage {
description
path
}
}
}
}
}
`;
Then, create a file named ProjectList.js
in your src/components
directory and make it look like this:
import React from "react";
import { Link } from "gatsby";
import { getImageUrl } from "takeshape-routing";
import routes from "../routes";
const ProjectListItem = ({ project }) => {
const startYear = new Date(project.startDate).getFullYear();
const endYear = project.endDate
? new Date(project.endDate).getFullYear()
: null;
const includeEndYear = endYear && startYear !== endYear;
return (
<Link to={routes.project(project.name)} className="project">
<img
className="project__thumbnail"
src={getImageUrl(project.coverImage.path, {
h: 200,
w: 300,
fit: "crop"
})}
alt={project.description}
/>
<p className="project__name">
<strong>{project.name}</strong>
</p>
<p class="project__metadata">
{startYear}
{includeEndYear ? `&endash; ${endYear}` : ""}
</p>
</Link>
);
};
const ProjectList = ({ projects }) => (
<ul className="nostyle project-list">
{projects.items.map((project, i) => (
<li className="project-list--entry" key={i}>
<ProjectListItem project={project} />
</li>
))}
</ul>
);
export default ProjectList;
In this file, the ProjectList
component renders a set of ProjectListItem
components. Each ProjectListItem
displays some limited information about the project and links to the Project page you’ve already made. This component doesn’t require a query to be included with it, since it’s not a standalone page and the data is provided by the index.js
file that includes it.
At this point, you should be able to run gatsby develop
, navigate to http://localhost:8000/
, and see a list of projects. Clicking on any one of them should take you to that project’s page. It’s alive!
About and 404 Pages
Finally, you’ll finish off by creating a standalone About page and a standalone 404 page. For the about page, you can create a file about.js
in src/pages
that looks like this:
import React from "react";
import { graphql } from "gatsby";
import { getImageUrl } from "takeshape-routing";
import Layout from "../layouts/default";
const AboutPage = ({ data }) => (
<Layout>
<article className="about">
<img
className="about__portrait"
src={getImageUrl(data.takeshape.about.portrait.path, {
h: 150,
w: 150,
fit: "crop"
})}
/>
<div
className="about__biography"
dangerouslySetInnerHTML={{ __html: data.takeshape.about.biography }}
/>
</article>
</Layout>
);
export default AboutPage;
export const query = graphql`
query {
takeshape {
about: getAbout {
biography: biographyHtml
portrait {
title
description
path
}
}
}
}
`;
The file should export your AboutPage
component and a query so that the page can render on its own.
For the 404 page, you’ll just wrap a few lines with your Layout
component and call it a day:
import React from "react";
import Layout from "../layouts/default";
const NotFoundPage = () => (
<Layout>
<h1>NOT FOUND</h1>
<p>You just hit a route that doesn't exist... the sadness.</p>
</Layout>
);
export default NotFoundPage;
Preview your production site
Running gatsby develop
lets you run a development-friendly version of your site, with neat features like hot-reloading of components when you make changes.
To preview what your site will look like in production, you can use the gatsby build
and gatsby serve
commands:
- After running
gatsby build
, your website will be compiled and saved into a folder namedpublic
at the top of your project. - After running
gatsby serve
, you can visit your production preview atlocalhost:9000
in your browser.
Deploy your fancy new site to Netlify
Now that your project is completed, it’s time to get it online! By using Gatsby, you’ll be circumventing TakeShape’s built-in static site generator, so you will need to set up your deployment system manually using a combination of Netlify’s continuous integration features and TakeShape’s webhooks functionality.
First make sure that your project is hosted in a remote Git repository, like on GitHub. Then create a New site from Git from the Netlify sites page. Authorize Netlify to access your project’s repository, then use the default build configuration. Netlify should automatically recognize that you’re using Gatsby as your static site generator and configure your build settings correctly. Then click Deploy Site.
Your first Deploy should fail. That’s ok! You haven’t added your TAKESHAPE_PROJECT
and TAKESHAPE_TOKEN
settings to your environment in TakeShape, so you should expect it to fail.
To fix this, go to your Netlify site’s settings and go to the “Build & Deploy” section. From there, you can edit your “Build environment variables” to securely add your TakeShape credentials. Click Edit variables and then add your variables for TAKESHAPE_PROJECT
and TAKESHAPE_TOKEN
. Click Save.
Then, go to your site’s Deploy section and click Trigger deploy. You’ll watch your deployment script run and log messages as it goes. At the end, you should see success messages and finally a declaration that your “Site is live”! Congrats!!! 🎉 If you go back to your site’s overview, you should see a link to your newly live site.
Finally you’ll want to set up a Build webhook (Webhooks are available on all paid TakeShape plans) so that you can tell Netlify to rebuild your site when you create, update, or delete your content in TakeShape. Back in your Netlify site’s “Build & Deploy” settings, look for the section labelled “Build hooks”. Click Add build hook, give it a name like “TakeShape”, click Save, and then copy the URL that gets created for you.
Then, in TakeShape, navigate to your project’s webhook settings in Project Settings > Webhooks. Configure a new webhook so that the URL you just copied is the Webhook URL and the Resources that trigger it are Content: *
. And make sure you check the Create, Update, and Delete actions. Leave the Secret field blank and click Save.
Now, whenever you create a new content object, update an existing one, or delete a content object, it’ll trigger a new build of your site in Netlify. This way, your site is always up-to-date with your TakeShape project.
Conclusion
If you’ve made it this far, congratulations! You dove head-first into the JAMstack and survived. Hopefully, you now have a strong understanding of the power of a configurable CMS, an automatically-generated GraphQL API, and a powerful React-powered static site. JAMstack is the future of web development and you’re now at the cutting edge.
If you have any feedback, comments, or questions on our series, don’t hesitate to reach out. Your feedback is always welcome through our in-app live chat or via support@takeshape.io.
Interested In Headless CMS For Gatsby.js?
TakeShape is a headless GraphQL CMS that makes building JAMstack with Gatsby easier. At TakeShape we're building the best tools for managing content possible for the most creative designers and developers. Our project templates, make it easy to get started. Plus, pricing is downright affordable. Sign up for a free account and spend more time being creative!