In part 1 of this mini-series I wrote about the technology choices I made when I started building new web pages for my local condominium. If you havenΒ΄t done it already, read about why I chose React/Gatsby and Chakra UI on the frontend, Contentful as a headless CMS and Netlify for hosting everything. I also needed an authentication solution for those parts of the web site that should only be accessible for logged in residents only.
Building the foundation
Starting a Gatsby project is as simple as typing npm init gatsby
on the command line and answering a few simple questions (or gatsby new
if you have Gatsby-CLI installed). Gatsby will then set up a starter project for you, which you can then modify.
You will be asked which CMS you want to use for storing the content, and can choose between Wordpress, Contentful, Sanity, DatoCMS, Shopify or Netlify CMS. You can use almost anything else with Gatsby as well - but Gatsby can set up a number of things for you automatically if you choose one of the predefined options. You will also be asked if you want to have a specific styling system pre-installed, such as Sass, Styled components, Emiton, PostCSS or Theme UI.
However, I chose to start from scratch, and installed the various dependencies I needed as I went along with the project. I needed gatsby-source-contentful to get content from my Contentful headless CMS. And I wanted to make life a little easier for myself by creating the user interface with Chakra UI. I also needed some other packages, like dotenv to handle environmental variables (such as access tokens for Contentful, and other things I didn't want to include in the source code on Github).
When everything is set up, you will get a page that looks something like this when entering gatsby develop
on the command line and visiting http://localhost:8000
:
The first thing you should do, is of course to remove this dummy page.
In Gatsby, routing is as simple as creating a React component in the /src/pages
folder and exporting it. For example, if you export a component from the /src/pages/test.js
file, you will have a route on /test
(ie you can type localhost:8000/test
in the browser to reach it). The main page - ie the front page of the website - is /src/pages/index.js
. This is how my index.js file looks like on my finished website:
// ./src/pages/index.js
import * as React from 'react';
import SEO from '../components/seo';
import CookieConsent from '../components/cookieConsent';
import HeroWide from '../components/sections/hero-wide';
import ArticleGrid from '../components/sections/articleGrid';
const IndexPage = () => {
return (
<>
<SEO />
<CookieConsent />
<HeroWide />
<ArticleGrid />
</>
);
};
export default IndexPage;
Normally I would include a Layout component here, for consistent layout with header, footer, etc. across all pages. But since I use Chakra UI, I have put the Layout component elsewhere where the Layout component is wrapped by <ChakraProvider>
which is necessary for it all to work. This also enables theme based styling using Chakra UI. I created the file ./src/chakra-wrapper.js
:
// ./src/chakra-wrapper.js
import * as React from 'react';
import { ChakraProvider } from '@chakra-ui/react';
import Layout from './components/layouts/layout';
import theme from './theme/';
export const wrapPageElement = ({ element }) => {
return (
<ChakraProvider resetCSS theme={theme}>
<Layout>{element}</Layout>
</ChakraProvider>
);
};
And then, in ./gatsby-browser.js
and ./gatsby-ssr.js
:
import * as React from 'react';
import { wrapPageElement as wrap } from './src/chakra-wrapper';
.
.
.
export const wrapPageElement = wrap;
This means that the entire page is wrapped in ChakraProvider, and then the Layout component which wraps everything else and includes a header and a footer. In the <ChakraProvider>
component in the top code snippet, I also pass in the theme I have defined for the page as a prop.
I ended up with the folder structure below, where I have put all reusable React components in /src/components
, pages under /src/pages
, page templates under /src/templates
and Chakra UI themes under /src/theme
:
src
βββ components
β βββ article.tsx
β βββ layouts
β β βββ layout.tsx
β βββ private-components
β βββ sections
β βββ articleGrid.tsx
β βββ footer.tsx
β βββ header.tsx
β βββ hero-wide.tsx
βββ pages
β βββ 404.tsx
β βββ index.tsx
β βββ informasjon.tsx
β βββ min-side.tsx
βββ templates
β βββ blog-archive-template.tsx
β βββ blog-template.tsx
β βββ page-template.tsx
βββ theme
β βββ colors.js
β βββ components
β β βββ button.js
β β βββ heading.js
β β βββ text.js
β βββ index.js
β βββ renderRichTextOptions.js
β βββ styles.js
β βββ textStyles.js
βββ utils
βββ privateRoute.tsx
As you can see, I chose to rename the .js files for the React components to .tsx, in order to use TypeScript in my components - and to reduce the risk of bugs when I pass data as props between components.
Fetch content from Contentful
As mentioned, I wanted to use Contentful for my content. Contentful is a headless CMS system, which means that the content is decoupled or separated from the front end. This makes it relatively easy if I later on wants to switch to another frontend or backend, or fetch content from the same source to another web page or maybe a mobile app. When using Gatsby, you can fetch content from virtually any source by making GraphQL queries in your code. There are a lot of ready-made plugins that make this very easy, whether you want to fetch data from markdown files, a headless CMS like Contentful or Sanity, or from an online shopping solution like Shopify. I used Gatsby's official Contentful plugin, gatsby-source-contentful.
When you have installed and configured the plugin, you can visit localhost:8000/__graphiql
to create the GraphQL queries. In the left column of the GraphiQL interface you get a view of all available data (including content from Contentful). The middle column is for creating the queries - and the column on the right shows the result of a query after you press the Run button. GraphiQL makes it very easy and straightforward to test different queries and check that you get back the data you expect, before copying the query into your code.
But before I could see my data in GraphiQL, I had to set up everything in Contentful. I first had to define a Content model, which is a description of the different types of content - and what fields should be available for each content type. For example, I have a content type called Blog Post, which contains fields such as Title, Summary, Top Image, Body Text and Author. For each of the fields you must define the type of the content - such as text, numbers, boolean values, media (images, video, etc). You can also create references between different types of content, for example links between a blog post and one or more authors (where Author is a content type as well).
I defined separate content types for front page text and pages (for example information pages and contact pages). In addition, I created a content type called Service menu, which is used to change a menu with information for the residents of the condominium - including links for downloading meeting minutes, a list of all the residents and other useful information. All content in the Service menu will require login.
Generation of static web pages
One of the things that makes websites created in Gatsby extremely fast is that Gatsby generates static web pages. This means that when you run gatsby build
, Gatsby will retrieve content from Contentful (or other sources) and build every HTML page for you. Thus, 100/100 in Lighthouse should be within reach:
As I have mentioned, all components exported from the /src/pages
folder will automatically be converted to static HTML pages. But to be able to programmatically create my own pages for each blog post and other content, I used one of Gatsby's built-in APIs, createPages. To explain:
When you build a Gatsby page, code in the gatsby-node.js
file will run once before the page is built. The createPages
(plural) API allows you to run a GraphQL query to retrieve content (such as blog posts) - in our case from Contentful. Then you can run a so-called action called createPage
(singular) on each blog post. createPage
receives as a parameter the React component you want to use as a page template, along with context data that the page template will receive as props. Context data in my case is the ID of the article in Contentful. Inside the page template you run a new GraphQL query where you only retrieve the blog post that has the correct ID, and then you retrieve everything you need to display the content - such as title, introduction, body text, images, etc. The page template is like any normal React component.
My gatsby-node.js looks like this (abbreviated - there are also several GraphQL queries and actions to create other types of pages. See my Github for full source code):
// ./gatsby-node.js
const path = require(`path`);
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions;
const blogPostTemplate = path.resolve(`src/templates/blog-template.tsx`);
.
.
.
return graphql(`
{
publicPosts: allContentfulBlogPost(
filter: { privatePost: { eq: false } }
) {
nodes {
contentful_id
slug
}
}
}
`).then((result) => {
if (result.errors) {
throw result.errors;
}
const blogNodes = (result.data.publicPosts || {}).nodes || [];
// Create public blog post pages.
// Skip private pages (in graphQl query)
blogNodes.forEach((node) => {
const id = node.contentful_id;
const slug = node.slug;
createPage({
// Path for this page β required
path: `/blog/${slug}`,
component: blogPostTemplate,
context: { id },
});
});
.
.
.
}
In the file blog-template.tsx
I fetch the blogposts one-by-one from Contentful using the GraphQL query below. Note the variable $id
in the GraphQL query. This ID comes from the context parameter sent from createPage
in gatsby-node.js
and gives us the content for the correct blog post, and nothing morge.
// ./src/templates/blog-template.tsx
export const query = graphql`
query BlogPostQuery($id: String!) {
contentfulBlogPost(contentful_id: { eq: $id }) {
title
createdAt(formatString: "DD.MM.YYYY")
updatedAt(formatString: "DD.MM.YYYY")
author {
firstName
lastName
}
excerpt {
excerpt
}
bodyText {
raw
references {
... on ContentfulAsset {
contentful_id
__typename
title
description
gatsbyImageData(layout: CONSTRAINED, aspectRatio: 1.6)
}
}
}
featuredImage {
gatsbyImageData(layout: CONSTRAINED, aspectRatio: 1.6)
file {
url
}
title
description
}
}
}
`;
Then I destructure the data I want from the query and use the data in the page template component:
// ./src/templates/blog-template.tsx
.
.
.
const {
title,
author,
createdAt,
updatedAt,
bodyText,
excerpt,
featuredImage,
} = contentfulBlogPost;
return (
<>
<SEO
title={title}
image={featuredImage?.file?.url || null}
description={excerpt?.excerpt || null}
/>
<Article
title={title}
bodyText={bodyText}
createdAt={createdAt}
updatedAt={updatedAt}
mainImage={featuredImage}
author={author}
buttonLink='/blog'
/>
</>
);
}
.
.
.
Since I often need to present content in article format, with a featured image, title, introduction, author, etc., I created an <Article>
component for this, and pass data to it via props.
One challenge I encountered was how to render content that was defined as Rich Text in Contentful. Content in Rich Text fields are based on blocks, and when you do a GraphQL query, you're getting back JSON containing nodes with all the content in. There are a lot of different ways to render this content, and Contentful has a little more info here. I used import {renderRichText} from 'gatsby-source-contentful/rich-text'
and then I could use {renderRichText (bodyText, renderRichTextOptions)}
in my Article component to render the contents of bodyText. renderRichTextOptions
is a component I import at the beginning of the <Article>
component, and inside renderRichTextOptions
I can then define how for example a <H1>
title or image should be rendered (<Text>
and <Heading>
in the code below are Chakra UI components):
// ./src/theme/renderTichTextOptions.js
.
.
.
const renderRichTextOptions = {
renderMark: {
[MARKS.BOLD]: (text) => <strong>{text}</strong>,
[MARKS.UNDERLINE]: (text) => <u>{text}</u>,
[MARKS.ITALIC]: (text) => <em>{text}</em>,
},
renderNode: {
[BLOCKS.PARAGRAPH]: (node, children) => (
<Text
textAlign='left'
my={4}
fontSize={{ base: 'sm', sm: 'md', md: 'lg' }}
>
{children}
</Text>
),
[BLOCKS.HEADING_1]: (node, children) => (
<Heading as='h1' textAlign='left' size='4xl'>
{children}
</Heading>
),
.
.
.
It's also possible to use another library, rich-text-react-renderer, but the way I did it worked very well and gave me the flexibility I needed.
Styling
Chakra UI have components for everything you need to create beautiful web pages, using components like <Badge>
, <Alert>
, <Text>
, <Heading>
, <Menu>
, <Image>
, and so on.
As Chakra UI is a theme based component library, you dont't have to write a single line of CSS. Instead, you just customize the standard theme if you want a different look-and-feel.
With Chakra UI you get responsive design right out of the box, with pre-defined breakpoints (which you can change if you want). You do not need to manually create media queries, but can do as I have done in the example below in your JSX code ( is a Chakra UI component intended for titles and renders by default an <H2>
- tag, but in the example I have chosen to render it as <H1>
):
<Heading
as='h1'
fontSize={['4xl', '6xl', '6xl', '7xl']}
textAlign={['center', 'left', 'left', 'left']}
pb={4}
>
Here we define font size and text alignment for four different screen sizes. That's actually all you need to do to get perfect responsive design with Chakra UI.
Or you could do like this to use Chakra UI's CSS Grid component and define that you want 1 column on small and medium screen sizes, and 2 columns on larger screens:
<Grid
templateColumns={{
sm: 'repeat(1, 1fr)',
md: 'repeat(1, 1fr)',
lg: 'repeat(2, 1fr)',
xl: 'repeat(2, 1fr)',
}}
pt={16}
gap={16}
mb={16}
mt={0}
maxWidth='95vw'
minHeight='45vh'
>
With Chakra UI you also get a web site with excellent accessibility, without you needing to think about aria-tags or other things.
Have a look at https://chakra-ui.com for more info and more examples.
Next step: Authentication and protected routes
Feel free to take a look at the finished website here: https://gartnerihagen-askim.no
The project is open source, you can find the source code at my Github.
This is a translation, the original article in Norwegian is here: Del 2: Slik bygget jeg sameiets nye nettsider. Grunnmuren er pΓ₯ plass
Top comments (0)