An Extensive Comparison of Gatsby & Next.js (While Rebuilding My Portfolio)
I was looking at my developer portfolio recently and decided it could use an update. While I kept it up to date with my most recent projects that I wanted to showcase, I wasn't a fan of the look anymore. I also thought it would be nice to use Contentful as a headless CMS to manage the content of the site, as well as fetch and display a couple of my most recent blog posts from here on dev.to.
I've mostly used Gatsby both at work and for personal projects, and I've been wanting to learn more about Next.js and become more familiar with using it, so I took this opportunity to start rebuilding my portfolio using both of these frameworks. I had a list of requirements in mind, so along the way I noted my experience with both frameworks and I want to share that experience with you here. Perhaps you will find it helpful the next time you need to decide which framework to use.
Requirements
I like to keep my portfolio nice and simple. The order of the sections are:
- Hero (including nav)
- Projects
- About Me
- Blog Posts
- Contact
- Footer
It's a single page, so I will not be looking to create dynamic routes / pages for any of my content. I would consider doing so for the blog posts since Contentful has a Markdown text box, and on dev.to you write in Markdown, so it would just be a copy paste. However, I wanted to see what would be considered best practice / convention for both:
- Fetching content from Contentful using GraphQL
- Fetching blog posts from the dev.to API using the good ol' Fetch API
A few other things I wanted to compare between the two were:
- Downloading and starting with a basic boilerplate with TypeScript support
- Integrating styled-components
- A few points regarding Contentful:
- The use of environment variables
- Using webhooks to automatically update the hosted site whenever I make a change in Contentful
- Similarly, how changes in Contentful appear during local development
- Local development experience
- Using fragments for reusable queries
- Optimizing images from Contentful
- How easy is it to host a Gatsby and Next.js site
I also want to note that my intention wasn't to fully rebuild my portfolio with each, but more so I wanted to get the scaffolding done, compare my notes as well as any pros and cons of the requirements I've listed above, and from there I'll decide which one I plan to proceed with to complete the project.
I simply created 2 new branches on my existing portfolio repo, which I aptly named gatsby
and next
, and worked with each framework on their respective branch. In hindsight, it would've been much easier to just work with framework each in their own folders in isolation. Oh well! ¯\_(ツ)_/¯
So, let's get started!
Boilerplate
To start, I looked at what possible starter / boilerplates both frameworks offered. While both offer Contentful based starters, they are not in TypeScript OOTB. So, I decided upon using their basic TypeScript starter and building it up from there. That way, I have as close as a starting point for each as possible.
Next.js has supported TypeScript for a while now, but thankfully Gatsby also now has full TypeScript support too. Not just with pages and components, which they also have supported for a while now, but all of the Gatsby API files (gatsby-browser
, gatsby-node
, etc.) can also be written in TypeScript as of Gatsby v4.8 and 4.9.
For Gatsby, you can run the command:
npm init gatsby -ts
And for Next.js you can run the command:
npx create-next-app@latest --ts
A quick, minor note regarding Gatsby: in the documentation it says to run npm init gatsby
and select TypeScript as your language when prompted. Or you can run npm init gatsby -ts
to skip that question. In my experience, that is not true. When you run npm init gatsby -ts
, you still have to select TypeScript as your language. So, it doesn't matter whether or not you include the -ts
flag.
For Next.js, there is no questionnaire during setup, and I actually don't mind this at all, and it kind of makes sense if you think about it. Gatsby is focused on creating fast, static sites using headless CMSs. As such, it gives you a few options to pick from during setup in the CLI, as well as a few options for styling your app.
With Next, since it is a hybrid framework and it can be used for much more than static headless CMS-based sites, including a questionnaire is not ideal because there are just so many things to consider. Perhaps including a step for styling would be nice, and some other common inclusions such as linting, but I think it's fine without one.
Either way, both startup just fine after running npm run dev
!
Styled Components
This is where things start to get interesting. styled-components is my go to approach for styling React apps myself. Otherwise, I usually go with Material UI (now MUI) or React Bootstrap.
Implementing styled-components into Gatsby and Next are quite different.
First, let's take a look at Gatsby.
Gatsby
With Gatsby, you will need to install the following NPM packages:
babel-plugin-styled-components
gatsby-plugin-styled-components
styled-components
And if you're using TypeScript like I do, then the @types
package from DefinitelyTyped as well:
@types/styled-components
So, in total 3-4 packages. From there, you simply need to add gatsby-plugin-styled-components
to your plugins
array inside your gatsby-config
file. After that, I typically create a styles
folder inside the src
folder, and then inside the styles
folder I create 2 files:
GlobalStyle.ts
theme.ts
theme.ts
will hold variables for the project such as colours, font sizes, breakpoints, etc. Here's the starting code I typically use:
const theme = {
mediaQueries: {
desktopHD: 'only screen and (max-width: 1920px)',
desktopMedium: 'only screen and (max-width: 1680px)',
desktopSmall: 'only screen and (max-width: 1440px)',
laptop: 'only screen and (max-width: 1366px)',
laptopSmall: 'only screen and (max-width: 1280px)',
tabletLandscape: 'only screen and (max-width: 1024px)',
tabletMedium: 'only screen and (max-width: 900px)',
tabletPortrait: 'only screen and (max-width: 768px)',
mobileXLarge: 'only screen and (max-width: 640px)',
mobileLarge: 'only screen and (max-width: 576px)',
mobileMedium: 'only screen and (max-width: 480px)',
mobileSmall: 'only screen and (max-width: 415px)',
mobileXSmall: 'only screen and (max-width: 375px)',
mobileTiny: 'only screen and (max-width: 325px)'
},
shades: {
white: '#fff',
black: '#000'
},
greys: {
// add any greys here
},
colors: {
primary: {
// add primary colors here
},
secondary: {
// add secondary colors here
}
},
fonts: {
// add font-families here
},
fontWeights: {
thin: 100,
extraLight: 200,
light: 300,
normal: 400,
medium: 500,
semiBold: 600,
bold: 700,
extraBold: 800,
black: 900
},
fontSizes: {
// add font sizes here
}
};
export default theme;
And GlobalStyle.ts
will use the createGlobalStyle
helper function to create a GlobalStyle
component. This component will house global styles and any styling resets you want to include. Here is what I typically start with:
import { createGlobalStyle } from 'styled-components';
import theme from './theme';
// destructured theme properties
const { mediaQueries } = theme;
const GlobalStyle = createGlobalStyle`
html {
box-sizing: border-box;
font-size: 100%;
@media ${mediaQueries.desktopSmall} {
font-size: 87.5%;
}
}
body {
/* add custom font-family here */
line-height: 1;
}
*,
*::before,
*::after {
box-sizing: inherit;
color: inherit;
font-size: inherit;
-webkit-font-smoothing: antialiased;
margin: 0;
padding: 0;
}
button,
input,
textarea {
/* add custom font-family here */
}
img,
svg {
border: 0;
display: block;
height: auto;
max-width: 100%;
}
a {
&:link,
&:visited {
text-decoration: none;
}
@media (hover) {
&:hover,
&:active,
&:focus {
outline: 0;
text-decoration: underline;
}
}
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
audio,
canvas,
video {
display: inline-block;
max-width: 100%;
zoom: 1;
}
`;
export default GlobalStyle;
From here, we open the Layout
component and wrap everything with the ThemeProvider
component provided by styled-components
. ThemeProvider
must have a theme
prop, so we can import the theme
above. We will also place the GlobalStyle
component as a child inside the ThemeProvider
. In the end, your Layout
file should look something like this:
const Layout = ({ children, title }) => (
<ThemeProvider theme={theme}>
<GlobalStyle />
<Seo title={title} />
<Navigation />
<main>{children}</main>
<Footer />
</ThemeProvider>
);
And that's it! Now, any styled components we create will have access to our theme via props.theme
like so:
background-color: ${(props) => props.theme.shades.white};
And just as a quick note, we can use destructuring to shorten it a bit:
background-color: ${({ theme }) => theme.shades.white};
In the end, a pretty quick setup! Now let's take a look at Next.js.
Next.js
With Next.js, we install all the same packages we would with Gatsby, except gatsby-plugin-styled-components
of course.
From there, we can create a styles
folder and also create the GlobalStyle.ts
& theme.ts
files as we did with Gatsby.
Next, while with Next.js you would typically have a Layout
component, you actually do not want to wrap that component with ThemeProvider
. Instead, you want to wrap everything in the _app.tsx
file in the pages
folder like so:
Before:
const MyApp = ({ Component, pageProps }: AppProps) => <Component {...pageProps} />;
export default MyApp;
After:
const MyApp = ({ Component, pageProps }: AppProps) => (
<ThemeProvider theme={theme}>
<Layout>
<GlobalStyle />
<Component {...pageProps} />
</Layout>
</ThemeProvider>
);
export default MyApp;
And we're done! Actually, no we're not. At this point, using styled-components will work just fine...sort of. If you either:
- Manually refresh the page in the browser
- Host this project, on Vercel or Netlify, for example
The styles will likely break. This is because the server-side rendering does not fetch the styles before rendering the page. To correct this issue, we need to inject server-side rendered styles to the head
so it can render the page and its styles correctly.
Inside the pages
folder, we need to create a file called _document.tsx
. In that file, you will need this:
// next
import Document, { Head, Html, Main, NextScript } from 'next/document';
import type { DocumentContext, DocumentInitialProps } from 'next/document';
// styled components
import { ServerStyleSheet } from 'styled-components';
class MyDocument extends Document {
static getInitialProps = async (ctx: DocumentContext): Promise<DocumentInitialProps> => {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />)
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
)
};
} finally {
sheet.seal();
}
};
render() {
return (
<Html>
<Head>
<link rel='preconnect' href='https://fonts.googleapis.com' />
<link rel='preconnect' href='https://fonts.gstatic.com' crossOrigin='true' />
<link
href='https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700;900&display=swap'
rel='stylesheet'
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
A BIG shoutout to Nicolás M. Pardo here on dev.to as he posted this in a comments section from a different article and it saved the day! And as you can see, I also just added the necessary code to include links to Google Fonts. If you don't need that, you can simply get rid of it.
Now we just need to restart the server and everything should be a-ok!
Contentful
In this section, we'll take a look at a few key areas:
- The use of environment variables
- Querying data from Contentful using GraphQL
- The use of webhooks
- Local development experience
- Using fragments for reusable queries
The use of environment variables
Somewhat disappointingly, while going through the setup for Gatsby, after selecting Contentful as your headless CMS, the necessary .env
files are not created as part of the setup. And with Next, you will need to set them up manually yourself.
Now to be fair to both, if you use the starter-gatsby-blog from Contentful themselves, the new gatsby-starter-contentful-homepage from Gatsby, or the Next.js Contentful example, these do use environment variables. It's just these basic starters that do not.
For Next.js, you simply need to create a .env.local
file, and add in your variables like so:
VARIABLE_NAME=VALUE
Then restart your local dev server if it is currently running so Next.js can load in the variables. You should see Loaded from env
in your terminal after the local dev server starts up.
For Gatsby, create 2 files:
.env.development
.env.production
Add in the same variables in both files and you will be good to go.
Querying data from Contentful using GraphQL
Both frameworks take a similar approach to querying data from Contentful using GraphQL. To greatly oversimplify things, the process essentially boils down to:
- Make a query using GraphQL
- Pass that returned data to your components as props
- Display the data in your components
How each framework does that is of course different, and we'll explore it in further detail below.
But first, one thing I do want to give big props to is Gatsby's implementation of the GraphiQL playground. Specifically, including an Explorer. I have to admit, I have taken this for granted, as it makes structuring queries so, so much easier.
With Next.js, I ended up using the GraphiQL playground provided by Contentful themselves. You can view it here:
Just make sure to update the placeholders with your actual Contentful Space ID and Access Token.
Gatsby
With Gatsby, querying data from Contentful using GraphQL is very straightforward. In your page component file, you add your GraphQL query typically beneath the export default
:
// component code up here...
export default Home;
export const homePageQuery = graphql`
query HomePageQuery {
contentfulHero(id: { eq: "ID_HERE" }) {
title
tagline
backgroundImage {
id
description
gatsbyImageData
}
}
}
`;
In this example, I'm querying data from a specific Hero model I created, and I want the values from the title
, tagline
, and backgroundImage
fields. I'll be discussing images in more detail later on.
To actually consume this data, the page component can access the returned data via the data
prop. From there, you can pass it down as a prop
to the corresponding component like so:
const Home = ({ data, location }: PageProps<GraphQLResult>) => {
// just double check...
console.log('data: ', data);
return (
<Layout title='Home'>
<Hero contentfulData={data.contentfulHero} />
</Layout>
);
};
With TypeScript, we can also type out how the response will look. The name I've always used is GraphQLResult
, but you can name it whatever you like. The type for this is just above the component like so:
type GraphQLResult = {
contentfulHero: ContentfulHero;
};
const Home = ({ data, location }: PageProps<GraphQLResult>) => {
// just double check...
console.log('data: ', data);
return (
<Layout title='Home'>
<Hero contentfulData={data.contentfulHero} />
</Layout>
);
};
I like to keep my types in a separate folder, so I import ContentfulHero
wherever I need it. The type looks like this:
import type { IGatsbyImageData } from 'gatsby-plugin-image';
export type ContentfulHero = {
title: string;
tagline: string;
backgroundImage: {
id: string;
description: string;
gatsbyImageData: IGatsbyImageData;
};
};
And the Hero component can also use this type for props
:
type HeroProps = {
contentfulData: ContentfulHero;
};
const Hero = ({ contentfulData }: HeroProps) => (
<div className='wrapper'>
<h1>{contentfulData.title}</h1>
<p>{contentfulData.tagline}</p>
</div>
);
export default Hero;
This way I have full type checking for my data, as well as some nice auto-completion when using the data in my component.
There are some tools you can use to automatically generate the types for your GraphQL Schema, such as GraphQL Code Generator. However, since this is a fairly small project, and I am the only dev working on it, I don't mind setting things up manually myself.
Next.js
With Next.js, the process is quite similar. However, there are multiple ways to query data with Next.js:
- React hooks at the component level (
useEffect
with anasync
function is a common approach) -
getStaticProps()
at the page level -
getServerSideProps()
at the page level -
useSWR()
hook at the component level
For my purposes, since this is a static portfolio site, I will be using the getStaticProps()
function. As outlined in the Next.js documentation, if the data is coming from a headless CMS, then getStaticProps()
is a great choice. Check out the documentation on how the function works if you are not familiar with it, as I do not have the courage to explain it since I am still new to it.
Inside this function, we can make our request. This video from the Contentful YouTube channel was very helpful, and required minimal modification for TypeScript.
In the video, Lee recommends using the graphql-request
package. And I have to say, as a first time user, I quite liked it! I will definitely look to use this again in the future!
To start, we will need to import GraphQLClient
& gql
from graphql-request
as named imports:
import { GraphQLClient, gql } from 'graphql-request';
Then inside the getStaticProps()
function, we can start with the following:
const endpoint = `https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`;
const options = {
headers: {
authorization: `Bearer ${process.env.CONTENTFUL_ACCESS_TOKEN}`
}
};
const graphQLClient = new GraphQLClient(endpoint, options);
Notice the value for endpoint
is different than that of the URL you would visit in the browser!
Next, we can use the gql
function to write our GraphQL Query, and we'll see some noticeable differences:
const homePageQuery = gql`
{
hero(id: "ID_HERE") {
backgroundImage {
sys {
id
}
description
height
url
width
}
tagline
title
}
}
`;
The first is actually ID_HERE
. When using the Explorer in Gatsby's GraphiQL playground, you can either use the ContentfulId or Id. The latter looks like this:
a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1
Where the As and 1s could be any combination of letters and numbers. With Next.js, you don't get this type of ID. Instead, you simply use the ENTRY ID
for whichever piece of content you are querying for.
To see this ENTRY ID
, select any piece of content you have created in Contentful. Then on the right-hand sidebar menu, select the Info
tab. Second from the top is the ENTRY ID
. In all fairness, you could also use this in your GraphQL queries with Gatsby as well. I've always just used the long-form id above out of habit.
The next difference are the fields we can query for the image. We'll need these files to pass them on to Next.js's <Image />
component. Again, we'll discuss each framework's approach to images and image optimization later.
Finally, store the returned data in a variable like so:
const data: ContentfulResponse = await graphQLClient.request(homePageQuery);
Then return in a props object:
return {
props: {
data
}
};
Here, I am using the ES6 object shorthand where if the key and value are the same, we can write like I have above instead of like this:
return {
props: {
data: data
}
};
So all together, getStaticProps()
looks like this:
export async function getStaticProps() {
// get contentful data
const endpoint = `https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`;
const options = {
headers: {
authorization: `Bearer ${process.env.CONTENTFUL_ACCESS_TOKEN}`
}
};
const graphQLClient = new GraphQLClient(endpoint, options);
const homePageQuery = gql`
query HomepageQuery {
hero(id: "ID_HERE") {
backgroundImage {
sys {
id
}
description
height
url
width
}
tagline
title
}
}
`;
const data: ContentfulResponse = await graphQLClient.request(homePageQuery);
return {
props: {
data
}
};
}
Now in the Home
page component, we can access this data
prop like so:
const Home: NextPage<HomeProps> = ({ data }) => {
console.log('data: ', data);
return (
<div>
<Hero />
</div>
);
};
And to be clear, data
is just the name I chose. I could've called it hero
if I wanted to, and accessed it via a hero
prop instead. I like using data
because I'll be adding on to the GraphQL query later on querying for multiple pieces of content. So, data
is just a generic variable name I like to use, and comes from the habit of using Gatsby.
And just like before with Gatsby, we pass the data down to the corresponding component:
const Home: NextPage<HomeProps> = ({ data }) => {
console.log('data: ', data);
return (
<div>
<Hero contentfulData={data.contentfulHero} />
</div>
);
};
And that's it! That's how we can query data from Contentful with both frameworks with GraphQL. Personally, I don't really have a preference. I think both are perfectly fine.
And as a quick side note, I would extract the querying logic in getStaticProps()
into a separate file and folder to make the code inside getStaticProps()
cleaner, as well as make the query a bit more reusable. It would require some modification, but it would make for a good starting point.
The use of webhooks
When it comes to hosting, whether you choose Netlify, Gatsby Cloud, or Vercel, they all have options to set up both environment variables, as well as webhooks that you can use with Contentful. This way, whenever you make a change, or mutliple changes, in Contentful, the site will rebuild with the latest data.
Local development experience
An interesting note here is that with Gatsby, during local development, whenever you make a change in Contentful, it will not appear while the local dev server is running. You must restart the server, and clean the public
and .cache
folders as well. So, I would recommend adjusting the dev
script in package.json
to the following:
"clean": "gatsby clean",
"dev": "npm run clean && gatsby develop",
This is because Gatsby will run your GraphQL queries during build time, and not run time. Therefore, a restart is required.
With Next.js however, you simply need to manually refresh the page in the browser, and any changes will be seen.
Using fragments for reusable queries
The great thing about querying data with GraphQL is you can use something called fragments. Fragments help by cutting down on repeated query fields.
Gatsby
With Gatsby, creating reusable fragments is very straightforward. In the src
folder, I typically create another folder called graphql
. In the graphql
folder, I then create a file called fragments.ts
. Here, let's create a highly reusable fragment for fields for an image:
export const imageFields = graphql`
fragment ImageFields on ContentfulAsset {
id
description
gatsbyImageData(placeholder: BLURRED)
}
`;
What's so nice about this is that Gatsby automatically exports these queries to the global scope. So, these fragments are available in any other GraphQL query in the entire project! We can even use our fragments in other fragments!
And just as a quick side note, I like to include id
here even if I'm only querying for a single image because in the instance I do query for multiple images, I have the id
's ready to use for mapping over the array and using them as the value for the key
prop.
Now, let's say for example we have a standard 50/50 model, with heading
& copy
on the left and an image
on the right. For the fragment, it would look something like this:
export const fiftyFiftyFields = graphql`
fragment FiftyFiftyFields on ContentfulFiftyFifty {
id
heading
copy {
copy
}
image {
...ImageFields
}
}
`;
Now we can use this fragment in the HomePageQuery
, for example, like so:
export const homePageQuery = graphql`
query HomePageQuery {
firstFiftyFifty: contentfulFiftyFifty(id: { eq: "ID_1_HERE" }) {
...FiftyFiftyFields
}
secondFiftyFifty: contentfulFiftyFifty(id: { eq: "ID_2_HERE" }) {
...FiftyFiftyFields
}
}
`;
In this example, we can also see aliases being used. Aliases let you rename the result of a field to anything you want. This is helpful for when you are querying the same thing but with different arguments.
So, combining fragments and aliases really help make our queries shorter, cleaner, and easier to read and write.
Next.js
With Next.js, while we don't get automatic global scoped fragments, we can still create and use them, just with a little bit of extra work.
We can start with the same process by creating a fragments.ts
file inside of a graphql
folder. Let's create a reusable Image fragment:
import { gql } from 'graphql-request';
export const FRAGMENT_IMAGE = gql`
fragment ImageFields on Asset {
sys {
id
}
description
height
url
width
}
`;
Now back in the index page file, inside getStaticProps()
and then inside our gql
template string, we can go from this:
const homePageQuery = gql`
query HomepageQuery {
hero(id: "ID_HERE") {
backgroundImage {
sys {
id
}
description
height
url
width
}
tagline
title
}
}
# fragments
${FRAGMENT_IMAGE}
`;
To this:
const homePageQuery = gql`
query HomepageQuery {
hero(id: "ID_HERE") {
backgroundImage {
...ImageFields
}
tagline
title
}
}
# fragments
${FRAGMENT_IMAGE}
# make sure to import this!
`;
It may not look very powerful right now when querying a single piece of content, but I'm sure you can imagine if this homepage had say 8-10 sections, all of them containing images, having the same fields repeated over and over again would make the query very long and rather difficult to traverse.
Image Optimization
Both frameworks come with image optimization OOTB in the form of components. Next.js has <Image />, and Gatsby has both <StaticImage /> and <GatsbyImage />.
Let's explore their differences:
Gatsby
Gatsby comes with 2 different image components. When you should use which? The answer to that is very simple.
Is the image you're using the same every time the component is rendered? For example, the site logo, or any other picture located within the project locally? Then use <StaticImage />
.
Is the image you're using going to be passed into a component as a prop
, and / or coming from an external source, such as Contentful? Then use <GatsbyImage />
.
Since this article is all about Contentful, we'll be focusing on <GatsbyImage />
!
In the GraphiQL playground, whenever you want to query for an image (or multiple images), I'd recommend getting the following fields:
id
description
gatsbyImageData
With this, we can create a reusable fragment for querying those fields on images (known as Assets in Contentful):
export const imageFields = graphql`
fragment ImageFields on ContentfulAsset {
id
description
gatsbyImageData(placeholder: BLURRED)
}
`;
When you execute the query with these fields, you'll see a ton of data for the gatsbyImageData field. But don't worry, the <GatsbyImage />
component will handle all of that for us!
You would use that data like so:
<GatsbyImage image={contentfulData.image.gatsbyImageData} alt={contentfulData.image.description} />
You can see in my fragment above I also added (placeholder: BLURRED)
. This will do as the wording suggests! It will blur the image until the full res version is ready to use. This will help cut down on page load time and help improve performance.
Next.js
With Next.js, we get a single <Image />
component that we can use for both static and dynamic images. And very similarly to Gatsby, as long as we pass it the right data for its various props, it will handle all the heavy lifting for us!
And just like with Gatsby, we can create a highly reusable fragment like we did above in the previous section to query certain fields from Contentful. We would then use that data like so:
<Image
src={data.hero.backgroundImage.url}
alt={data.hero.backgroundImage.description}
width={data.hero.backgroundImage.width}
height={data.hero.backgroundImage.height}
/>
And that's it! Well...sort of. With Next.js, there is one more step. We have to add the domain these images are coming from to an array in our next.config.js
file. This array holds an array of domains
that are whitelisted. This tells Next.js that it is ok to fetch the images from these external sources and optimize them:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['images.ctfassets.net']
},
reactStrictMode: true
};
module.exports = nextConfig;
Ok, now we have optimized images from Contentful!
Blog Posts
Now that I'm writing blog posts here on dev.to, I wanted to incorporate them into my portfolio as well. While Contentful does have a Markdown text box that I could just copy and paste the post into and create dynamic routes with either framework, I was more interested in how I would query data from 2 different data sources using 2 different methods:
- Querying content from Contentful using GraphQL
- Querying posts from dev.to using REST API w/ Fetch
Let's take a look!
Gatsby
With Gatsby, I had an assumption of how I would query for my blog posts, and after some research (aka quick Googling), it seems like my assumption was correct.
I created a BlogPosts component, and inside that component I made the fetch call like so:
const BlogPosts = () => {
const [posts, setPosts] = useState<DevPost[]>([]);
const [isLoading, setIsLoading] = useState(false);
const fetchBlogPosts = async () => {
setIsLoading(true);
const response = await fetch('https://dev.to/api/articles?username=andrews1022');
const data: DevPost[] = await response.json();
setPosts(data);
setIsLoading(false);
};
useEffect(() => {
fetchBlogPosts();
}, []);
return (
<div>
<h2>Posts</h2>
<div>
{isLoading ? (
<p>Loading...</p>
) : (
posts.map((post) => (
<div key={post.id}>
<p>{post.title}</p>
</div>
))
)}
</div>
</div>
);
};
Now I realize I don't have error handling in the form of an error state, or a cleanup in my useEffect
, but that's ok since I'm just trying things out before making any large commitments.
This is a pretty standard approach:
- Have state for the data, loading, and error
- Write an async function that queries the data, and updates the various pieces of state accordingly
- In the useEffect hook, call the async function and have an empty array of dependencies so this function will only be executed once when the component is first mounted / rendered
And that's it! My blog post titles display just fine. With Next.js though, that's where things get a little spicy.
Next.js
With Next.js, we can take advantage of the getStaticProps()
function and add this additional request below the GraphQL query to Contentful and return it alongside the data
like so:
export async function getStaticProps() {
// make the contentful query up here...
// get dev.to data
const postsResponse = await fetch('https://dev.to/api/articles?username=andrews1022');
const posts: DevPost[] = await postsResponse.json();
return {
props: {
data,
posts
}
};
}
Again, I realize there isn't any error handling here, this is just for the sake of demonstration purposes. And in case I didn't mention earlier, the DevPost
type is a custom type I created.
With these posts, I can pass them down to my BlogPosts component:
<BlogPosts posts={posts} />
And then loop over them and display them like so (just the title
for now to make sure things are working):
type BlogPostsProps = {
posts: DevPost[];
};
const BlogPosts = ({ posts }: BlogPostsProps) => {
return (
<div>
<h2>Posts</h2>
<div>
{posts.map((post) => (
<div key={post.id}>
<p>{post.title}</p>
</div>
))}
</div>
</div>
);
};
Why I think this is spicy is because since Next.js pre-renders the page at build time using the props returned by getStaticProps()
, there is no awkward Loading...
text or spinner being shown temporarily. Everything is just...there! I'm still getting used to Next.js, so I probably didn't explain that very well but I just think it's neat (insert Marge Simpson meme here)!
Hosting
Hosting is dead simple no matter which you pick. Each framework has their own hosting service which they recommend:
Gatsby Cloud for Gatsby projects, and Vercel for Next.js projects.
You could also use Netlify if you wish.
Whichever you pick, you can select your project from GitHub (once you have given it authorization to do so), and the service will automatically build and host the site for you!
And as previously mentioned, you can include environment variables for your Contentful API keys, as well as setup webhooks with Contentful.
Final Thoughts
Whew, that was a lot! If you made it this far, thank you so much for reading this post. I had a lot of fun researching and trying out the various points I mentioned here, and it was great to try out and learn more about Next.js some more as well.
My goal for this article was to simply show you each framework's approach for common tasks and let you decide which approach(es) you do or don't like. Personally, I'm going to stick with Next.js to build out the rest of my portfolio simply because I want to become more familiar with it and learn more about it.
In my opinion, for a simple static like this, you really can't go wrong with either framework. If you have more experience with one over the other, I would recommend trying the other and see what you think. I provided my experience here and I hope you found it useful.
Cheers!