Originally published by William Imoh at https://scotch.io
What's the trend about static sites and decoupled development architectures with the JAMStack? Why are top sites and apps moving to build with "static site generators"? It's speed, security, affordability, developer experience and much more. We are here for it all.
Static sites are known over time as fast sites which serve static content from a CDN without any web server or API interaction. Modern build tools currently furnish sites with the speed and security of static sites along with the dynamism of fetching and interacting with data from APIs during runtime.
In this post, we will:
Gatsby is an open-source static site generator for building modern static sites. Gatsby offers the ability to build performant, secure and affordable deployable sites. Absolving the need for a web server, this mitigates the risk of security breaches pertinent to web servers. Gatsby also offers the rich developer experience by way of building the sites in React.js, thus merging the existing vibrant React.js ecosystem.
Since developing with Gatsby is done in React, application logic including server calls can be written in the component lifecycle methods or even as application-wide logic. These will be executed at runtime, hence the notion "Static-Dynamic". On the data layer of Gatsby applications, Gatsby uses GraphQL to make data queries.
Other static site generators abound the internet which allows the creation of static sites, the question is, how efficient are they at building these sites? With the Gatsby CLI, creating an optimized single-paged app is a command away, with little effort put towards performance optimizations.
Netlify offers a rich deployment experience for modern applications. Touting an intuitive CI/CD platform as well as utilizing and managing AWS Lambda functions directly on Netlify, deploying static-dynamic applications as we would in this tutorial becomes seamless.
In this post, we would create a netlify function to fetch images using the Unsplash API. This lambda function will be tested locally using netlify-lambda
and deployed alongside the application to Netlify.
Functions ship with a memory capacity of 128MB, this is just enough to handle our use case, however for more computational power and workload, you can increase the memory capacity of your functions.
Here's what we'll be building in this article:
For this tutorial, knowledge of HTML, CSS, JavaScript, and React is required.
Node.js and its package manager NPM is required for this app. Check if you have them installed with:
node -v && npm -v
This command would output the versions of both node and npm. If not installed, proceed to Nodejs.org to download and install node and its package manager.
Install the Gatsby CLI globally with:
npm i -g gatsby-cli
Once the CLI tool is installed, open a new folder and create a new Gatsby project using the default starter. Do this with:
gatsby new gastify
This command clones the default Gatsby starter into the folder and installs all required dependencies specified in the project.
Gatsby ships with starters which are preset websites or apps with which you can quickly build your site off.
Change directory to the project directory and install the required dependencies with:
cd gastify && npm i --save axios bulma react-infinite-scroll-component
Here's a summary of the components:
Once these are installed, start a development server with:
gatsby develop
This spins up a dev server on localhost:8000. Gatsby ships with a hot reloading feature, so any changes to the source file reload the page in the browser instantaneously.
The Gatsby project currently contains default files and configuration shipped with the starter. We will proceed to develop the layout and style.
Everything in Gatsby, just like React, is a component. Gatsby is unique in the way that it serves individually exported JS files in the src/pages
folder as a page in the app. Each page can be navigated to using the <Link/>
component in Gatsby.
In this project, individual components are written in src/components
. Each exported component can be reused in the entirety of the app.
Gatsby provides other default files to extend the functionality further or configure the app. gatsby-config.js
contains the config for all plugins used in the app. Those currently installed and configured were shipped along with the default starter.
The site layout will be developed as a component and reused across the application. The components/layout.js
file currently exports the layout of the app as designed in the starter. Remove all the content of the layout.js
file, import all required dependencies and components with:
import React from "react" import PropTypes from "prop-types" import { useStaticQuery, graphql } from "gatsby"import Header from “./header”
Here, React and prop-types were imported alongside useStaticQuery
and GraphQL. useStaticQuery
, shipped in Gatsby v2 allows us to make GraphQL data queries in non-page components. This data fetching is static and happens during build, hence the term Static Query.
The Header
component which houses the navbar is also imported in the component. This Header
component will be created shortly. Define and export the Layout
component with the following:
const Layout = ({ children }) => {
const data = useStaticQuery(graphqlquery SiteTitleQuery { site { siteMetadata { title } } }
)return (
<>
<Header siteTitle={data.site.siteMetadata.title} />
<div
style={{
margin:0 auto
,
maxWidth: 900,
padding:0px 1.0875rem 1.45rem
,
paddingTop: 0,
}}
>
<main>{children}</main>
</div>
</>
)
}Layout.propTypes = {
children: PropTypes.node.isRequired,
}export default Layout
Here the useStaticQuery
hook is used to fetch the site title from the siteMetadata. This siteTitle
is subsequently passed in the header component as props. The base style for the div
enclosing the children of the layout is also configured.
Go on ahead to gatsby-config.js
in the root directory to change the site title and description. Modify the siteMetada
property to:
siteMetadata: {
title:Gastify
,
description:Here lies a doggo Infinite scroll Image gallery built with Gatsby, Netlify Functions and the Unsplash API
,
author:@iChuloo
,
},
In components/header.js
, import the required modules and components with:
import { Link } from “gatsby”
import PropTypes from “prop-types”
import React from “react”
Create the Header
functional component which defines a siteTitle
prop with:
const Header = ({ siteTitle }) => (
<header>
<nav className=“navbar is-dark” style={{ marginBottom: “2em” }}>
<div className=“navbar-brand”>
<Link
to=“/”
style={{
marginLeft: “3em”,
padding: “10px”,
}}
className=“has-text-white is-size-3”
>
{siteTitle} 🐶
</Link>
</div>
<div className=“navbar-end” style={{ marginRight: “3em” }}>
<div className=“navbar-item”>
<Link
to=“/”
style={{
padding: “10px”,
}}
className=“has-text-white”
>
Home
</Link>
<Link
to=“/gallery/”
style={{
padding: “10px”,
}}
className=“has-text-white”
>
Gallery
</Link>
</div>
</div>
</nav>
</header>
)export default Header
Two routes were designated in the navbar, while /
directs to the homepage, /gallery/
directs to the gallery page to be created shortly. The <Link/>
component shipped with gatsby is used to navigate the routes in the single-paged app and ships with its own to
attribute which accepts the slug of the route.
Bulma classes were used to style this component. For Bulma to work, we will import it in the index.js
page shortly.
Next, we will utilize this layout component on the homepage.
Home Page
In src/pages/index.js
, wipe the default starter code in the file and import required dependencies and components with:
import React from “react”
import { Link } from “gatsby”
import Layout from “…/components/layout”
import SEO from “…/components/seo”
import “bulma/css/bulma.min.css”
Notice how the minified Bulma CSS file was imported. Also, the Layout component was imported alongside the SEO component, which was included in the starter by default.
Define and export the page using the imported components with:
const IndexPage = () => (
<Layout>
<SEO title=“Home” />
<div className=“has-text-centered” style={{ marginTop: “20%” }}>
<h1 className=“is-size-2”>Welcome to Pride Rock! . . . or nah 😹</h1>
<p className=“is-size-5” style={{ marginTop: “2%” }}>
Find within, a fire doggo infinite image gallery built with Gatsby, and
Images served using Netlify functions from Unsplash. Perfecto!
</p>
<button className=“button is-dark is-large” style={{ marginTop: “10%” }}>
<Link to=“/gallery/” className=“has-text-white”>
Open Sesame! 🔥
</Link>
</button>
</div>
</Layout>
)export default IndexPage
Bulma classes were also used to style this page.
Gallery Page
In the same src/pages
directory, create a new folder named gallery.js
. Similar to the index page, import the SEO component as well as the Layout component. Do this with:
import React from “react”
import Layout from “…/components/layout”
import SEO from “…/components/seo”
Create and export the page component with:
const Gallery = () => {
return (
<Layout>
<SEO title=“Gallery” />
<h1 className=“is-size-3”>Images from Unsplash…</h1>
<p style={{ marginBottom: “5%” }}>
Now this is the Law of the Jungle, as old and true as the sky, for as
long as you keep scrolling, you shall find more doggo images 🐶 🐕.
</p>
</Layout>
)
}export default Gallery
Once you save the page, the browser should refresh with the complete pages though the gallery page will only have the text and no images yet.
You can restart the dev server to rebuild.
Like we stated earlier, the good thing about building with tools like Gatsby is we can make API calls in components and pass the data to the DOM during runtime, giving the experience of a dynamic app in a static environment. The react-infinite-scroll-component
will be used to create the infinite scroll whereas the Images will be fetched from Unsplash using netlify functions.
In src/components
create a new component named InfiniteImages.js
. In the component, import the required dependencies with:
import React from “react”
import PropTypes from “prop-types”
import InfiniteScroll from “react-infinite-scroll-component”
For this gallery, we’ll need two components:
These components can be split into further subcomponents but for the sake of simplicity, we’ll leave it at these two and even have them co-located in the same file.
In InfiniteImages.js
, create a functional component after the dependency imports named ImageGallery
. This is the gallery view. Define the components to accept props of images
, loading
, and fetchImages
. Do this with:
import …const ImageGallery = ({ images, loading, fetchImages }) => {
// Create gallery here
return (
<InfiniteScroll
dataLength={images.length}
next={() => fetchImages()}
hasMore={true}
loader={
<p style={{ textAlign: “center”, marginTop: “1%” }}>
More doggo incoming 🐕 🐕…
</p>
}
>
<div className=“image-grid”>
{!loading
? images.map(image => (
<div className=“image-item” key={image.id}>
<img src={image.urls.regular} alt={image.alt_description} />
</div>
))
: “”}
</div>
</InfiniteScroll>
)
}
The InfiniteScroll
component imported takes props of:
In the infinite scroll, the array of images passed through the images
prop is traversed and Images rendered in a grid. Properties of each image object are passed as data to the key
, alt
value and ultimately the image src
.
The masonry is styled with css-grid. Create a custom CSS file named gallery.css
in the components folder and edit it to:
.image-grid {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-auto-rows: minmax(50px, auto);
}.image-grid .image-item:nth-child(5n) {
grid-column-end: span 2;
}.image-grid img {
display: flex;
width: 100%;
height: 100%;
object-fit: cover;
}
Import the CSS file to the InfiniteImages.js
component with:
import “./gallery.css”
With the view complete, let’s set up the parent component handling fetch from the API.
Create a component in InfiniteImages.js
named InfiniteImages
.
This functional component uses the React useState
and useEffect
hooks to handle state and lifecycle computation, respectively. Axios is also used to make HTTP requests. Import Axios and the hooks in InfiniteImages.js
with:
import React, { useState, useEffect } from “react”
import axios from “axios”// Component logic below
With hooks imported, define the InfiniteImage
component with:
const InfiniteImages = () => {
// Hold state
const [images, setImages] = useState([])
const [loading, setLoading] = useState(true)// Fetch images on component mount
useEffect(() => {
fetchImages()
}, [])// API endpoint
const endpoint = “<Endpoint here once available>”// Fetch Images from functions
const fetchImages = () => {
axios(endpoint).then(res => {
setImages([…images, …res.data.images])
setLoading(false)
})
}
return (
<ImageGallery images={images} loading={loading} fetchImages={fetchImages} />
)
}
Here, we created state variables to hold the fetched images and also to store the state of the fetch operation. The images
variable defaults to an empty array while the loading variable defaults to true
as we assume by default, the images are loading once the component mounts.
The useEffect
hook is used to call the fetchImages
function once the component is mounted. An empty array as a second argument signifies that the function will only run when the component mounts.
A fetchImages
function is created which uses axios to make a GET request to a specified API. The returned data is spread in the images
array in state. Thus any new data fetched is added to existing ones in state. The loading
variable is also set to false as the fetch operation is complete.
The ImageGallery
component earlier created is used to render the gallery with the required data.
Assign the Proptypes to the ImageGallery
component and export the InfiniteImage
module with:
import…const ImageGallery = ({ images, loading, fetchImages }) => {
// Create gallery here
return (
// Component logic here
)
}const InfiniteImages = () => {
// Component logic here
}ImageGallery.propTypes = {
images: PropTypes.array,
loading: PropTypes.bool,
fetchImages: PropTypes.func,
}export default InfiniteImages
With this, we have the gallery set up, what’s missing is the API endpoint. We’ll proceed to define a Netlify function to handle that.
Netlify functions are AWS lambdas managed directly on Netlify. With this, we abstract the management and scaling of a traditional server. Multiple functions can be created and used across an application after the application is built.
These functions in JavaScript are .js
files stored in any designated directory in the project’s repository and specified on Netlify or in the netlify.toml config file in the root directory of a project.
Creating a Netlify function is seamless. It is either created in a functions directory or a build tool is used to create the function from a source folder into the functions folder.
If you are familiar with backend development, creating node servers and APIs would probably be a breeze, for Netlify Functions written in JavaScript, knowledge of Nodejs is not compulsory as Netlify ships Netlify-lambda. This is a tool to help with building Netlify functions from modern JavaScript workflows using webpack/babel.
We will employ this in this tutorial to build functions written in ES6 syntax into a functions directory.
For this gallery, we will query a free Unsplash API to obtain super cute images of dogs and display them in a masonry. Head to Unsplash, signup to create a new app and get an access key.
While this API can be consumed directly in the app, we would like to abstract this image fetching process to our API, thus securing access/private keys or any other backend logic. In other applications, business or other logic can also go in our API.
Install Functions Build tool
netlify-lambda
CLI is used to run functions locally as well as create a deployable build. These functions can be written with modern JS workflows before being bundled into a single build using webpack/babel. For this project, we’ll create a folder to house the locally written functions as well as the deployable functions.
Install the netlify-lambda CLI with:
npm i -g netlify-lambda
With this we can serve the functions locally with:
netlify-lambda serve <path-to-source-folder>
A deployable build can be created with :
netlify-lambda build <path-to-source-folder>
Create a Function and Source Directory
Create a folder in the src
directory named lambda
. This serves as the source folder will contain all the functions as written. Create a second folder in the root directory of the project named functions
. This, in turn, will hold all deployable functions. For Netlify to recognize the functions folder, we need to specify it in the Netlify config file. Create a file in the root directory of the project named netlify.toml
. Add this config with:
[build]
Functions = “functions”
This specifies that the functions for the app can be found in the functions directory in the app root. Once this app is deployed, each function will be available on:
https://<root-url-of-app>/.netlify/functions/<function-name>
Define The Fetch Function
Lambda functions are JS files which export a handler method. Each function takes three parameters:
In the project, create a file named fetch.js
in src/lambda
, configure the function with:
import axios from “axios”
import config from “…/…/config”exports.handler = function(event, context, callback) {
const apiRoot = “https://api.unsplash.com”
const accessKey = process.env.ACCESS_KEY || config.accessKeyconst doggoEndpoint =
${apiRoot}/photos/random?client_id=${accessKey}&count=${10}&collections='3816141,1154337,1254279'
axios.get(doggoEndpoint).then(res => {
callback(null, {
statusCode: 200,
body: JSON.stringify({
images: res.data,
}),
})
})
}
In here, axios which is imported is used to make an API call to the Unsplash API, and the returned data is sent in the callback with a status code of 200. The API Key will be stored as an environment variable on Netlify and also in a config file for local usage.
Create a file named config.js
in the root directory and add the API key from Unsplash with:
const config = {
accessKey: “<Add access key>”,
}export default config
Be sure to add the config file to .gitignore
so it’s not added to your repository if public.
State a dev server for the functions with:
netlify-lambda serve src/lambda
This creates a server on http://localhost:9000
.
Navigate to http://localhost:9000/fetch
to see the data fetched from the API being displayed on the browser.
To create a build in the functions folder, run:
netlify-lambda build src/lambda
This creates a build in the functions folder ready to be deployed.
We have the functions set up and running, let’s configure it for local usage.
Configure Function for Local Usage
Using the function as is in the local dev server on Gatsby would throw a CORS error. Advanced proxying in Gatsby.js, affords us the ability to use a proxy middleware to proxy the functions in our local environment consequently bypassing CORS. Install http-proxy-middleware with:
npm i --save-dev http-proxy-middleware
Configure the proxy middleware in gatsby-config.js with:
let proxy = require(“http-proxy-middleware”)module.exports = {
siteMetadata: {
// define site metadata
},
// Enables the use of function URLs locally
developMiddleware: app => {
app.use(
“/.netlify/functions/”,
proxy({
target: “http://localhost:9000”,
pathRewrite: { “/.netlify/functions/”: “” },
})
)
},
plugins: [
// define plugins
],
}
The /.netlify/functions/
endpoint in development is proxied to http://local:9000
and the path is re-written, this way http://localhost:9000/fetch
becomes /.netlify/functions/fetch
which is also the valid local endpoint for the function.
Pass the API endpoint to the axios request in the InfiniteImages
function in src/components/InfiniteImages.js
. Do this with:
import // …const ImageGallery = ({ images, loading, fetchImages }) => {
// Component logic here
}const InfiniteImages = () => {
// Hold state
const [images, setImages] = useState([])
const [loading, setLoading] = useState(true)// Fetch images on component mount
useEffect(() => {
fetchImages()
}, [])// Fetch Images from functions
const fetchImages = () => {
axios(“/.netlify/functions/fetch”).then(res => {
setImages([…images, …res.data.images])
setLoading(false)
})
}
return (
<ImageGallery images={images} loading={loading} fetchImages={fetchImages} />
)
}ImageGallery.propTypes = {
// Define proptypes
}export default InfiniteImages
Restart the Gatsby dev server, while ensuring the server of the local function is running. Here’s what the app looks now:
At this point, the app works on our local environment. Before deploying to Netlify, re-build the functions with:
netlify-lambda build src/lambda
Run a Gatsby build to verify the build process works without errors. Do this with:
gatsby build
Next is to deploy to a Git provider.
Push to Git Provider
Create a new repository on a remote git provider. Initialize the repository on your local machine, stage all your files, commit and push to the new remote repository.
Ensure the config.js
file in the root directory is added to .gitignore
.
Deploy On Netlify
Go on to Netlify and create an account. Use the “New site from Git” button to add a new site.
On the “Connect to Git Provider” tab, select GitHub or whichever provider you used and authorize your account. In the next tab, pick the repository you deployed to or use the search function to search for the repository.
Configure the deploy settings with the build command as gatsby build
and publish directory as public
. Click the “Show advanced” button then the “new variable” button to add a new environment variable.
Hit the “Deploy site” button to deploy.
Netlify deploys the site and utilizes the functions in the functions directory after build. These functions are not exposed on the client side except the functions directory is in the publish directory.
Note that the build command, publish directory and environment variables can also be set in the netlify.toml
file of the project.
You can find the final deployed site at https://gastify.netlify.com
You can find the repository here: https://github.com/Chuloo/gatsby-netlify-functions
Here’s a peek on lighthouse audit scores of the deployed site:
In this post, we saw an overview of static sites, their features, importance, going dynamic with fetching dynamic content using serverless functions and an overview of serverless functions.
#web-development #javascript #node-js #reactjs #graphql