The default Gatsby starter
For an overview of the project structure please refer to the Gatsby documentation - Building with Components
Install this starter (assuming Gatsby is installed) by running from your CLI:
gatsby new gatsby-wiki
- Start your Gatsby Development Environment
- Adding Content and Linking Pages
- Styling your JSX
- Adding Interactive Components
- Importing Components to your Sites
- Passing down Props
- Gatsby Plugins
- Single Page Application
- GraphQL
- Adding File Data
- Working with Markdown
- Adding Material-UI
- Adding Elasticsearch
- Build the Static Page
Now change into your site directory and run the Gatsby development environment using npm:
cd gatsby-wiki
npm run development
You can now access your website on https://localhost:8000 :
The /src/pages/index.js file contains regular JSX - add any HTML inside the /
import React from 'react'
import Link from 'gatsby-link'
const IndexPage = () => (
<div>
<h1>Hi people</h1>
<p>Welcome to your new Gatsby site.</p>
<p>Now go build something great.</p>
<Link to="/page-2/">Go to page 2</Link>
</div>
)
export default IndexPage
You need to import Link from gatsby-link to use the Link Component and link to other pages - above you see the:
<Link to="/page-2/">Go to page 2</Link>
component, linking our index.js page to another page inside the same folder with the name page-2.js. Every js file inside the /src/pages folder will automagically be routed by Gatsby!
You can use simply add inline styles to your component, e.g.
const IndexPage = () => (
<div style={{color: 'tomato', background: 'blue'}}>
<h1>Hi people</h1>
<p>Welcome to your new Gatsby site.</p>
<p>Now go build something great.</p>
<Link to="/page-2/">Go to page 2</Link>
</div>
)
For some advanced styles check out the Gatsby plugins Glamor or Styled Components.
How to install those plugins is explained below - Gatsby Plugins .
React allows you to add interaction to your page - we want to add a counter, set it's state to 0 on load and have two buttons that use onClick events to increment or decrement the state of the counter.
We can just add a new file /src/pages/counter.js and link to it from the index page <Link to="/counter/">Go to Counter</Link>:
import React from 'react'
class Counter extends React.Component {
constructor() {
super()
this.state = { count: 0 }
}
render() {
return <div>
<h1>Counter</h1>
<p>current count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>plus</button>
<button onClick={() => this.setState({ count: this.state.count - 1 })}>minus</button>
</div>
}
}
export default Counter
So far, we used every file inside the pages directory as a separate site. But React.js allows us to take the default component - that is exported at the bottom of the file - and import it into another page. For example, we could take the <Counter /> component above and add it to the index page (instead of just linking to it).
We just need to add an import line to the beginning of /src/pages/index.js:
import React from 'react'
import Link from 'gatsby-link'
import Counter from './counter'
And reference the Counter inside the JSX code of index.js, like this:
const IndexPage = () => (
<div>
<h1>Hi people</h1>
<p>Welcome to your new Gatsby site.</p>
<p>Now go build something great.</p>
<Link to="/page-2/">Go to Page 2</Link><br/><br/>
<Counter />
</div>
)
We can now pass properties, from the parent component, down to the Counter component - e.g. we can change the title of our counter, depending on the page it is displayed on:
<Counter header="This is the Index Counter" />
The prop header is now available to the render function inside the Counter component. Now we can get different headers for our Counter component, depending on the parent component that called it - awesome!
render() {
return <div>
<h3>{this.props.header}</h3>
<p>current count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>plus</button>
<button onClick={() => this.setState({ count: this.state.count - 1 })}>minus</button>
</div>
}
The same goes with styles - if we want the header to match the colour scheme of our parent component, we can just pass down a color prop to the Counter component:
<Counter header="This is the Index Counter" color="rebeccapurple" />
And add the necessary inline styles in the component itself:
render() {
return <div>
<h3 style={{color: this.props.color}}>{this.props.header}</h3>
<p>current count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>plus</button>
<button onClick={() => this.setState({ count: this.state.count - 1 })}>minus</button>
</div>
}
To be able to still open the localhost:8000/counter URL, we now have to define a default prop inside the counter component - the header tag and font colour will be undefined, if there is no parent component passing down props! This can be done by Prop-Types, that we need to install:
npm install --save prop-types
Now we can import it into /src/pages/counter.js :
import React from 'react'
import PropTypes from 'prop-types'
And define a default value for the header prop below the Counter component (above the export statement):
Counter.defaultProps = {
header: 'Default Counter',
color: 'black'
}
Plugins are Node.js packages that implement Gatsby APIs. They enable you to easily solve common website build problems e.g. setup Sass, add markdown support, process images, etc.
In this example, we want to use a plugin for NProgress.js to add a loading animation to our site. You install the NProgress plugin with npm:
npm install --save gatsby-plugin-nprogress
Now we have to tell Gatsby to use the plugin by editing (creating if file doesn't exist) the gatsby-config.js file inside the root directory of our app. Coming from the starter template, we already have the react-helmet plugin installed (This plugin is described below: Page Layout). Now simply add the gatsby-plugin-nprogress to the array:
module.exports = {
siteMetadata: {
title: `Gatsby Wiki`,
},
plugins: [
`gatsby-plugin-react-helmet`,
{
resolve: `gatsby-plugin-nprogress`,
options: {
// Setting a color is optional.
color: `rebeccapurple`,
// Disable the loading spinner.
showSpinner: false,
}
}
],
}
We now want to add a Serviceworker to our site that helps us cache important parts of our application, giving us a certain amount of offline support - as the Offline Plugin tells us, we will also install the Manifest Plugin (make sure, that it is listed before the Offline Plugin!).
npm install --save gatsby-plugin-manifest
npm install --save gatsby-plugin-offline
Now we add them to our Gatsby configuration:
module.exports = {
siteMetadata: {
title: `Gatsby Wiki`,
},
plugins: [
`gatsby-plugin-react-helmet`,
{
resolve: `gatsby-plugin-nprogress`,
options: {
// Setting a color is optional.
color: `rebeccapurple`,
// Disable the loading spinner.
showSpinner: false,
}
},
{
resolve: `gatsby-plugin-manifest`,
options: {
name: "Gatsby Wiki",
short_name: "Gatsby Wiki",
start_url: "/",
background_color: "white",
theme_color: "rebeccapurple",
display: "minimal-ui",
icons: [
{
// Everything in /static will be copied to an equivalent
// directory in /public during development and build, so
// assuming your favicons are in /static/favicons,
// you can reference them here
src: `/apple-touch-icon.png`,
sizes: `180x180`,
type: `image/png`,
},
{
src: `/favicon.ico`,
sizes: `256x256`,
type: `image/png`,
},
],
},
},
`gatsby-plugin-offline`,
],
}
Gatsby offers an easy way to create Single-Page-Applications (SPA's) with it's layout feature. You can find the JSX and CSS inside /src/layout. The Gatsby Starter, that we are using, already uses a header navbar, that is defined inside the index.js file (and comes with the necessary css).
You can see that the app already uses React-Helmet as a Gatsby plugin. This reusable React component will manage all of your changes to the document <head>. Helmet takes plain HTML tags and outputs plain HTML tags.
The layout defines a <Header /> component, that - together with the <Helmet /> component - is used inside the <TemplateWrapper />
All your content, from the pages that we created so far, is then injected into the Wrapper via the {children} tag. This way, you can create top-navbars, headers, side-navigations and footers, that are then displayed on all of your websites.
We can define some global variables inside gatsby-config.js in the root directory of our app:
module.exports = {
siteMetadata: {
title: `Gatsby Wiki`,
author: `Mike Polinowski`,
description: `Trying out Gatsby`
}
}
This Data will be available to every page and can be queried usind GraphQL. Just add the following GraphQL query to /src/pages/index.js, to get a hold of those values:
export const query = graphql`
query FirstQuery {
site {
siteMetadata {
title
author
description
}
}
}
`
Then we have to inject this {data} into the parent component <IndexPage />:
const IndexPage = ({data}) =>
Now we are able to query this data inside the component:
<h1>{data.site.siteMetadata.description}</h1>
Why is it data.site.siteMetadata? Gatsby's graphql debugger is running at https://localhost:8000/___graphql you can also use it to test your queries and see how the results look. Just open the debugger and try out our previous query:
With Gatsby you can use GraphQL to query Data from your files directly. Transformer plugins transform File nodes into various other types of data e.g. gatsby-transformer-json transforms JSON files into JSON data nodes and gatsby-transformer-remark transforms markdown files into MarkdownRemark nodes from which you can query an HTML representation of the markdown.
In this case we will use gatsby-source-filesystem to create file nodes from our file system.
npm install --save gatsby-source-filesystem
After installation, add the plugin to gatsby-config.js. You can have multiple instances of this plugin to read source nodes from different locations on your filesystem.
The following sets up the Jekyll pattern of having a pages directory for Markdown files and a data directory for .json, .yaml, .csv.:
{
resolve: `gatsby-source-filesystem`,
options: {
name: `pages`,
path: `${__dirname}/src/pages/`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `data`,
path: `${__dirname}/src/data/`,
},
}
You can now open the GraphiQL debugger put in curly brackets - when you start typing allFiles, it should offer autocompletion. Just press enter to accept and CTRL + ENTER again to fill out the query for all page ID's:
{
allFile {
edges {
node {
id
}
}
}
}
When you delete id and press CTRL + SPACE, you will be given a drop down menu with all options that you can query:
Using the parent, children and relativePath attribute enables you to create e.g. a breadcrumb navigation:
We can now add a GraphQL query to /src/pages/page-2.js to loop through all of our pages and display some data:
export const query = graphql`
query MyFilesQuery {
allFile {
edges {
node {
relativePath
prettySize
extension
birthTime(fromNow: true)
}
}
}
}
`
Don't forget to inject the {data} to the page component:
const SecondPage = ({data}) =>
Now we can add some JSX that loops through all of our files and outputs the information inside a <table>
<table>
<thead>
<tr>
<th>relativePath</th>
<th>prettySize</th>
<th>extension</th>
<th>birthTime</th>
</tr>
</thead>
<tbody>
{data.allFile.edges.map(({node}, index) =>
<tr key={index}>
<td>
{node.relativePath}
</td>
<td>
{node.prettySize}
</td>
<td>
{node.extension}
</td>
<td>
{node.birthTime}
</td>
</tr>
)}
</tbody>
</table>
Now we are able to access information about all of our pages. But as mentioned, in the beginning of the last paragraph, we are also able to use Gatsby Transformer Plugins to look into files and make their content available to GraphQL.
In this case we want to use Markdown files and transform them, to be able to display their content in our website. The Transformer Plugin needed for this is gatsby-transformer-remark. First we need to install the plugin:
npm install --save gatsby-transformer-remark
And add it to our gatsby-config.js:
plugins: [
`gatsby-transformer-remark`,
]
Then create a markdown page inside /src/pages/FirstMDpost/index.md that contains some FrontMatter (metadata in the beginning of the file, that can later be queried by GraphQL) and some text:
---
path: '/md-posts'
title: 'My first Post'
date: '2017-10-05'
author: 'Mike Polinowski'
chapter: 'Index'
---
# This is my first mardown Post!
Now we have Markdown available in GraphQL - as before, just start typing allMardownRemark (ENTER autocompletes) and then press CTRL + ENTER to complete your query:
Now we can query for the FrontMatter as well as the MD-to-HTML transformed content of each MD file we add to our pages folder:
The markdown represents the data that is going to be displayed. But now we need to create a style template that is used with this data. Lets start by adding a new folder inside /src called templates. Now add a file to it called post.js that will contain the structure template for every post entry. The file contains the JSX markup for our post:
import React from 'react'
export default function Template({data}) {
const {markdownRemark: post} = data
return (
<div>
<h1>{post.frontmatter.title}</h1>
<div dangerouslySetInnerHTML={{__html: post.html}} />
</div>
)
}
export const postQuery = graphql`
query BlogPostByPath($path: String!) {
markdownRemark(frontmatter: { path: { eq: $path} }) {
html
frontmatter {
path
title
}
}
}
`
The <Template /> component receives {data} props, that are retrieved by an GraphQL query.
The query looks for a markdown post, where the called URL equals the $path given inside it's frontmatter. So if the URL that you type into your browser was /md-posts, a markdown file with a path: '/md-posts' inside it's frontmatter, would be a hit.
The query then uses the markdownRemark plugin to transform the post markdown to HTML and make both the path and title from it's frontmatter available iside {data}, that is passed down into the component and then rendered.
Gatsby is already configured to route all pages inside /src/pages as pages for our website. But now we have to register our posts, that are from the markdown files and the post.js template. To do this, we have to create a file named gatsby-node.js inside the root directory of our app. We are going to use the createPages Gatsby API to create pages from our post template:
const path = require('path');
exports.createPages = ({boundActionCreators, graphql}) => {
const {createPage} = boundActionCreators;
// const createPage = boundActionCreators.createPage;
const postTemplate = path.resolve('src/templates/post.js');
return graphql(`{
allMarkdownRemark {
edges {
node {
html
id
frontmatter {
path
title
}
}
}
}
}`)
.then(res => {
if(res.errors) {
return Promise.reject(res.errors);
}
res.data.allMarkdownRemark.edges.forEach(({node}) => {
createPage({
path: node.frontmatter.path,
component: postTemplate
})
})
})
}
Save and restart your app - then open https://localhost:8000/md-posts inside your web browser - Voila` !
To create children post for the ./src/pages/FirstMDpost/index.md file, we can simply add more files to the folder and define nested routes inside their frontmatter - e.g. ./src/pages/FirstMDpost/myfirstpost.md:
---
path: '/md-posts/first-post'
title: 'First Blog Post'
date: '2017-10-05'
author: 'Mike Polinowski'
chapter: 'Markdown Posts'
---
# This is my first markdown Post!
and ./src/pages/FirstMDpost/mysecondpost.md:
---
path: '/md-posts/second-post'
title: 'Second Blog Post'
date: '2017-10-05'
author: 'Mike Polinowski'
chapter: 'Markdown Posts'
---
# A dive into Markdown Syntax
They will be accessible via https://localhost:8000/md-posts/first-post and https://localhost:8000/md-posts/second-post respectively.
We can now use GraphQL to retrieve all of our Markdown pages and apply filter to them. For this test, we will just add the a table to our start page, showing the last 10 posts (I know we only made 3 so far...), we want to order them descending by date and only display pages that are inside the chapter: 'Markdown Posts', which will exclude our index.md:
const IndexPage = ({data}) => (
<div>
<h2>Markdown Index</h2>
<p>The table below sorts out all Markdown pages that are not inside the "Markdown Posts" chapter - as defined inside their frontmatter. It also applies a filter, to only display the latest 10 posts. Click on here to display
<Link to="/md-posts/">
all Markdown pages
</Link>
.</p>
<table>
<thead>
<tr>
<th>Date</th>
<th>Link</th>
</tr>
</thead>
<tbody>
{data.allMarkdownRemark.edges.map(post => (
<tr key={post.node.id}>
<td>
{post.node.frontmatter.date}
</td>
<td>
<Link
to={post.node.frontmatter.path}>
{post.node.frontmatter.title}
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
export const pageQuery = graphql`
query IndexQuery {
allMarkdownRemark(limit: 10
sort: {fields: [frontmatter___date], order: DESC}
filter: { frontmatter: { chapter: {eq: "Markdown Posts"} }}
) {
edges {
node {
id
frontmatter {
path
title
date
}
}
}
}
}
`
Once you start adding links inside your Markdown files, you will notice that clicking them will reload your application - which isn't good :( But no worries here is gatsby-plugin-catch-links coming to your rescue! And the nice thing about it - you install it, add to your Gatsby plugins inside ./gatsby-config.js and it just works:
npm install --save gatsby-plugin-catch-links
// In your gatsby-config.js
plugins: [
`gatsby-plugin-catch-links`,
]
Sweet!
To make our life easier, we want to include ready-to-use material design components from the guys @ Material-UI. We are going to install the beta version of v.1.0.0 - which also requires the Roboto Fontface and the Material-UI Icons:
npm install material-ui@next --save
npm install typeface-roboto --save
npm install material-ui-icons --save
We can now easily import Material-UI components into our app:
import React from 'react'
import { render } from 'react-dom'
import Button from 'material-ui/Button'
import 'typeface-roboto'
function AppWithButton() {
return (
<Button>
Hello World
</Button>
);
}
render(<AppWithButton />, document.querySelector('#app'));
One of the pre-requisites for this project is, that we need to create a lightning-fast interface for our ElasticSearch Index. We already build the ES6 Class component for it. And adding it to Gatsby / Material-UI turned out to be surprisingly straight-forward.
First, add ./src/pages/search/jsx and modify the ElasticSearch Component to play nice with our UI:
import React, { Component } from 'react'
import Link from 'gatsby-link'
import elasticsearch from 'elasticsearch'
import { withStyles } from 'material-ui/styles'
import Grid from 'material-ui/Grid'
import Button from 'material-ui/Button'
import ResultCards from '../components/ResultCards'
const connectionString = 'localhost:9200'
const _index = 'wiki2_de_2017_09_09'
const _type = 'article'
let client = new elasticsearch.Client({
host: connectionString,
log: "trace"
})
const rootStyle = {
flexGrow: 1,
marginTop: 30,
}
export class Search extends Component {
constructor(props) {
super(props)
this.state = { results: [] };
this.handleChange = this.handleChange.bind(this)
}
handleChange(event) {
const search_query = event.target.value;
client.search({
index: _index,
type: _type,
body: {
query: {
multi_match: {
query: search_query,
fields: ['title^100', 'tags^100', 'abstract^20', 'description^10', 'chapter^5', 'title2^10', 'description2^10'],
fuzziness: 1,
},
},
},
}).then(function(body) {
this.setState({ results: body.hits.hits });
}.bind(this),
function(error) {
console.trace(error.message);
}
);
}
render() {
return (
<div className="container">
<input type="text" onChange={this.handleChange} />
<SearchResults results={this.state.results} />
</div>
);
}
}
const SearchResults = ({results}) => (
<div className="search_results">
<br/><hr/>
<div className={rootStyle}>
<Grid container spacing={24}>
{results.map((result , i) =>
<ResultCards key={i}
image={result._source.image}
title={result._source.title2}
link={result._source.link}
abstract={result._source.abstract}/>
)}
</Grid>
</div>
<br/><br/><Link to="/" style={{ textDecoration: 'none' }}><Button raised color="primary">Go back to the homepage</Button></Link>
</div>
)
export default Search
The <SearchResults /> component iterates over the Material UI card inside <ResultCards />:
import React from 'react'
import Link from 'gatsby-link'
import Card, { CardActions, CardContent, CardMedia } from 'material-ui/Card'
import Button from 'material-ui/Button'
import Typography from 'material-ui/Typography'
import Grid from 'material-ui/Grid'
const ResultCards = ({image, title, abstract, link}) => (
<Grid item xs={12} sm={6} lg={4}>
<Card style={{ maxWidth: 345 }}>
<CardMedia
style={{ height: 200 }}
image={image}
title={abstract}
/>
<CardContent>
<Typography type="headline" component="h4" style={{ minHeight: 60, marginBottom: "10px" }}>
{title}
</Typography>
<Typography component="p" style={{ minHeight: 50, marginBottom: "10px" }}>
{abstract}
</Typography>
</CardContent>
<CardActions>
<Link to={link} style={{ textDecoration: 'none' }}>
<Button dense color="primary">
Read
</Button>
</Link>
<Button dense color="primary">
Learn More
</Button>
</CardActions>
</Card>
</Grid>
)
export default ResultCards
and adds the results from the ElasticSearch JSON response - giving us a nice responsive card grid (the images used below are not inside this repository - just add a few PNG files (597x382) to ./public/images/Search, named according to the image URL defined inside our ElasticSearch Index:
We now want to move our website from the development environment to our webserver. Gatsby offers us a simple command to build render our React.js page into a static website:
npm run build
You can find the output inside the /public folder of your Gatsby App.